From 13d0eb22f9fe76229b5ae7d3a1b20668c024a8dc Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 5 Mar 2021 11:10:06 -0500 Subject: [PATCH 1/6] refactor: fix implementation of SFC :slotted id handling fix #2892 --- .../__snapshots__/scopeId.spec.ts.snap | 51 ++--- .../compiler-core/__tests__/scopeId.spec.ts | 41 ++-- packages/compiler-core/src/codegen.ts | 60 ++---- packages/compiler-core/src/runtimeHelpers.ts | 8 +- .../compiler-ssr/__tests__/ssrScopeId.spec.ts | 41 ++-- .../__tests__/ssrSlotOutlet.spec.ts | 52 ++++- .../src/transforms/ssrTransformComponent.ts | 6 + .../src/transforms/ssrTransformSlotOutlet.ts | 15 +- .../__tests__/components/KeepAlive.spec.ts | 8 +- .../__tests__/helpers/renderSlot.spec.ts | 4 +- .../__tests__/helpers/scopeId.spec.ts | 105 ----------- .../runtime-core/__tests__/scopeId.spec.ts | 178 ++++++++++++++++++ packages/runtime-core/__tests__/vnode.spec.ts | 10 +- packages/runtime-core/src/apiInject.ts | 2 +- packages/runtime-core/src/component.ts | 6 +- .../src/componentPublicInstance.ts | 6 +- .../src/componentRenderContext.ts | 57 ++++++ .../runtime-core/src/componentRenderUtils.ts | 17 +- packages/runtime-core/src/componentSlots.ts | 2 +- .../runtime-core/src/components/KeepAlive.ts | 1 + .../runtime-core/src/components/Suspense.ts | 55 ++++-- .../runtime-core/src/components/Teleport.ts | 13 +- packages/runtime-core/src/directives.ts | 2 +- .../runtime-core/src/helpers/renderSlot.ts | 4 + .../runtime-core/src/helpers/resolveAssets.ts | 2 +- packages/runtime-core/src/helpers/scopeId.ts | 36 ---- .../src/helpers/withRenderContext.ts | 37 ---- packages/runtime-core/src/hydration.ts | 29 ++- packages/runtime-core/src/index.ts | 9 +- packages/runtime-core/src/renderer.ts | 118 ++++++++---- packages/runtime-core/src/vnode.ts | 21 ++- .../server-renderer/__tests__/render.spec.ts | 30 ++- .../__tests__/ssrScopeId.spec.ts | 127 +++++++++++-- .../src/helpers/ssrRenderComponent.ts | 6 +- .../src/helpers/ssrRenderSlot.ts | 6 +- packages/server-renderer/src/render.ts | 15 +- 36 files changed, 723 insertions(+), 457 deletions(-) delete mode 100644 packages/runtime-core/__tests__/helpers/scopeId.spec.ts create mode 100644 packages/runtime-core/__tests__/scopeId.spec.ts create mode 100644 packages/runtime-core/src/componentRenderContext.ts delete mode 100644 packages/runtime-core/src/helpers/scopeId.ts delete mode 100644 packages/runtime-core/src/helpers/withRenderContext.ts diff --git a/packages/compiler-core/__tests__/__snapshots__/scopeId.spec.ts.snap b/packages/compiler-core/__tests__/__snapshots__/scopeId.spec.ts.snap index 7af32808b17..4076e61db6a 100644 --- a/packages/compiler-core/__tests__/__snapshots__/scopeId.spec.ts.snap +++ b/packages/compiler-core/__tests__/__snapshots__/scopeId.spec.ts.snap @@ -1,51 +1,48 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`scopeId compiler support should push scopeId for hoisted nodes 1`] = ` -"import { createVNode as _createVNode, toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from \\"vue\\" -const _withId = /*#__PURE__*/_withScopeId(\\"test\\") +"import { createVNode as _createVNode, toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, openBlock as _openBlock, createBlock as _createBlock, setScopeId as _setScopeId } from \\"vue\\" -_pushScopeId(\\"test\\") +_setScopeId(\\"test\\") const _hoisted_1 = /*#__PURE__*/_createVNode(\\"div\\", null, \\"hello\\", -1 /* HOISTED */) const _hoisted_2 = /*#__PURE__*/_createVNode(\\"div\\", null, \\"world\\", -1 /* HOISTED */) -_popScopeId() +_setScopeId(null) -export const render = /*#__PURE__*/_withId((_ctx, _cache) => { +export function render(_ctx, _cache) { return (_openBlock(), _createBlock(\\"div\\", null, [ _hoisted_1, _createTextVNode(_toDisplayString(_ctx.foo), 1 /* TEXT */), _hoisted_2 ])) -})" +}" `; exports[`scopeId compiler support should wrap default slot 1`] = ` -"import { createVNode as _createVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId } from \\"vue\\" -const _withId = /*#__PURE__*/_withScopeId(\\"test\\") +"import { createVNode as _createVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\" -export const render = /*#__PURE__*/_withId((_ctx, _cache) => { +export function render(_ctx, _cache) { const _component_Child = _resolveComponent(\\"Child\\") return (_openBlock(), _createBlock(_component_Child, null, { - default: _withId(() => [ + default: _withCtx(() => [ _createVNode(\\"div\\") ]), _: 1 /* STABLE */ })) -})" +}" `; exports[`scopeId compiler support should wrap dynamic slots 1`] = ` -"import { createVNode as _createVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, renderList as _renderList, createSlots as _createSlots, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId } from \\"vue\\" -const _withId = /*#__PURE__*/_withScopeId(\\"test\\") +"import { createVNode as _createVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, renderList as _renderList, createSlots as _createSlots, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\" -export const render = /*#__PURE__*/_withId((_ctx, _cache) => { +export function render(_ctx, _cache) { const _component_Child = _resolveComponent(\\"Child\\") return (_openBlock(), _createBlock(_component_Child, null, _createSlots({ _: 2 /* DYNAMIC */ }, [ (_ctx.ok) ? { name: \\"foo\\", - fn: _withId(() => [ + fn: _withCtx(() => [ _createVNode(\\"div\\") ]) } @@ -53,39 +50,29 @@ export const render = /*#__PURE__*/_withId((_ctx, _cache) => { _renderList(_ctx.list, (i) => { return { name: i, - fn: _withId(() => [ + fn: _withCtx(() => [ _createVNode(\\"div\\") ]) } }) ]), 1024 /* DYNAMIC_SLOTS */)) -})" +}" `; exports[`scopeId compiler support should wrap named slots 1`] = ` -"import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, createVNode as _createVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId } from \\"vue\\" -const _withId = /*#__PURE__*/_withScopeId(\\"test\\") +"import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, createVNode as _createVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\" -export const render = /*#__PURE__*/_withId((_ctx, _cache) => { +export function render(_ctx, _cache) { const _component_Child = _resolveComponent(\\"Child\\") return (_openBlock(), _createBlock(_component_Child, null, { - foo: _withId(({ msg }) => [ + foo: _withCtx(({ msg }) => [ _createTextVNode(_toDisplayString(msg), 1 /* TEXT */) ]), - bar: _withId(() => [ + bar: _withCtx(() => [ _createVNode(\\"div\\") ]), _: 1 /* STABLE */ })) -})" -`; - -exports[`scopeId compiler support should wrap render function 1`] = ` -"import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId } from \\"vue\\" -const _withId = /*#__PURE__*/_withScopeId(\\"test\\") - -export const render = /*#__PURE__*/_withId((_ctx, _cache) => { - return (_openBlock(), _createBlock(\\"div\\")) -})" +}" `; diff --git a/packages/compiler-core/__tests__/scopeId.spec.ts b/packages/compiler-core/__tests__/scopeId.spec.ts index 710c65bff8d..3c1d6d554b7 100644 --- a/packages/compiler-core/__tests__/scopeId.spec.ts +++ b/packages/compiler-core/__tests__/scopeId.spec.ts @@ -1,12 +1,13 @@ import { baseCompile } from '../src/compile' -import { - WITH_SCOPE_ID, - PUSH_SCOPE_ID, - POP_SCOPE_ID -} from '../src/runtimeHelpers' +import { SET_SCOPE_ID } from '../src/runtimeHelpers' import { PatchFlags } from '@vue/shared' import { genFlagText } from './testUtils' +/** + * Ensure all slot functions are wrapped with _withCtx + * which sets the currentRenderingInstance and currentScopeId when rendering + * the slot. + */ describe('scopeId compiler support', () => { test('should only work in module mode', () => { expect(() => { @@ -14,25 +15,12 @@ describe('scopeId compiler support', () => { }).toThrow(`"scopeId" option is only supported in module mode`) }) - test('should wrap render function', () => { - const { ast, code } = baseCompile(`
`, { - mode: 'module', - scopeId: 'test' - }) - expect(ast.helpers).toContain(WITH_SCOPE_ID) - expect(code).toMatch(`const _withId = /*#__PURE__*/_withScopeId("test")`) - expect(code).toMatch( - `export const render = /*#__PURE__*/_withId((_ctx, _cache) => {` - ) - expect(code).toMatchSnapshot() - }) - test('should wrap default slot', () => { const { code } = baseCompile(`
`, { mode: 'module', scopeId: 'test' }) - expect(code).toMatch(`default: _withId(() => [`) + expect(code).toMatch(`default: _withCtx(() => [`) expect(code).toMatchSnapshot() }) @@ -48,8 +36,8 @@ describe('scopeId compiler support', () => { scopeId: 'test' } ) - expect(code).toMatch(`foo: _withId(({ msg }) => [`) - expect(code).toMatch(`bar: _withId(() => [`) + expect(code).toMatch(`foo: _withCtx(({ msg }) => [`) + expect(code).toMatch(`bar: _withCtx(() => [`) expect(code).toMatchSnapshot() }) @@ -65,8 +53,8 @@ describe('scopeId compiler support', () => { scopeId: 'test' } ) - expect(code).toMatch(/name: "foo",\s+fn: _withId\(/) - expect(code).toMatch(/name: i,\s+fn: _withId\(/) + expect(code).toMatch(/name: "foo",\s+fn: _withCtx\(/) + expect(code).toMatch(/name: i,\s+fn: _withCtx\(/) expect(code).toMatchSnapshot() }) @@ -79,19 +67,18 @@ describe('scopeId compiler support', () => { hoistStatic: true } ) - expect(ast.helpers).toContain(PUSH_SCOPE_ID) - expect(ast.helpers).toContain(POP_SCOPE_ID) + expect(ast.helpers).toContain(SET_SCOPE_ID) expect(ast.hoists.length).toBe(2) expect(code).toMatch( [ - `_pushScopeId("test")`, + `_setScopeId("test")`, `const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, "hello", ${genFlagText( PatchFlags.HOISTED )})`, `const _hoisted_2 = /*#__PURE__*/_createVNode("div", null, "world", ${genFlagText( PatchFlags.HOISTED )})`, - `_popScopeId()` + `_setScopeId(null)` ].join('\n') ) expect(code).toMatchSnapshot() diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 20f7e33ec88..cd572313286 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -43,9 +43,7 @@ import { SET_BLOCK_TRACKING, CREATE_COMMENT, CREATE_TEXT, - PUSH_SCOPE_ID, - POP_SCOPE_ID, - WITH_SCOPE_ID, + SET_SCOPE_ID, WITH_DIRECTIVES, CREATE_BLOCK, OPEN_BLOCK, @@ -197,12 +195,11 @@ export function generate( indent, deindent, newline, - scopeId, ssr } = context + const hasHelpers = ast.helpers.length > 0 const useWithBlock = !prefixIdentifiers && mode !== 'module' - const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module' const isSetupInlined = !__BROWSER__ && !!options.inline // preambles @@ -212,7 +209,7 @@ export function generate( ? createCodegenContext(ast, options) : context if (!__BROWSER__ && mode === 'module') { - genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined) + genModulePreamble(ast, preambleContext, isSetupInlined) } else { genFunctionPreamble(ast, preambleContext) } @@ -229,14 +226,7 @@ export function generate( ? args.map(arg => `${arg}: any`).join(',') : args.join(', ') - if (genScopeId) { - if (isSetupInlined) { - push(`${PURE_ANNOTATION}_withId(`) - } else { - push(`const ${functionName} = ${PURE_ANNOTATION}_withId(`) - } - } - if (isSetupInlined || genScopeId) { + if (isSetupInlined) { push(`(${signature}) => {`) } else { push(`function ${functionName}(${signature}) {`) @@ -301,10 +291,6 @@ export function generate( deindent() push(`}`) - if (genScopeId) { - push(`)`) - } - return { ast, code: context.code, @@ -375,23 +361,20 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) { function genModulePreamble( ast: RootNode, context: CodegenContext, - genScopeId: boolean, inline?: boolean ) { const { push, - helper, newline, - scopeId, optimizeImports, - runtimeModuleName + runtimeModuleName, + scopeId, + mode } = context - if (genScopeId) { - ast.helpers.push(WITH_SCOPE_ID) - if (ast.hoists.length) { - ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID) - } + const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module' + if (genScopeId && ast.hoists.length) { + ast.helpers.push(SET_SCOPE_ID) } // generate import statements for helpers @@ -434,13 +417,6 @@ function genModulePreamble( newline() } - if (genScopeId) { - push( - `const _withId = ${PURE_ANNOTATION}${helper(WITH_SCOPE_ID)}("${scopeId}")` - ) - newline() - } - genHoists(ast.hoists, context) newline() @@ -480,7 +456,7 @@ function genHoists(hoists: (JSChildNode | null)[], context: CodegenContext) { // push scope Id before initializing hoisted vnodes so that these vnodes // get the proper scopeId as well. if (genScopeId) { - push(`${helper(PUSH_SCOPE_ID)}("${scopeId}")`) + push(`${helper(SET_SCOPE_ID)}("${scopeId}")`) newline() } @@ -493,7 +469,7 @@ function genHoists(hoists: (JSChildNode | null)[], context: CodegenContext) { }) if (genScopeId) { - push(`${helper(POP_SCOPE_ID)}()`) + push(`${helper(SET_SCOPE_ID)}(null)`) newline() } context.pure = false @@ -817,15 +793,11 @@ function genFunctionExpression( node: FunctionExpression, context: CodegenContext ) { - const { push, indent, deindent, scopeId, mode } = context + const { push, indent, deindent } = context const { params, returns, body, newline, isSlot } = node - // slot functions also need to push scopeId before rendering its content - const genScopeId = - !__BROWSER__ && isSlot && scopeId != null && mode !== 'function' - if (genScopeId) { - push(`_withId(`) - } else if (isSlot) { + if (isSlot) { + // wrap slot functions with owner context push(`_${helperNameMap[WITH_CTX]}(`) } push(`(`, node) @@ -855,7 +827,7 @@ function genFunctionExpression( deindent() push(`}`) } - if (genScopeId || isSlot) { + if (isSlot) { push(`)`) } } diff --git a/packages/compiler-core/src/runtimeHelpers.ts b/packages/compiler-core/src/runtimeHelpers.ts index f40c94c3d89..5172b8c7f95 100644 --- a/packages/compiler-core/src/runtimeHelpers.ts +++ b/packages/compiler-core/src/runtimeHelpers.ts @@ -25,9 +25,7 @@ export const CAMELIZE = Symbol(__DEV__ ? `camelize` : ``) export const CAPITALIZE = Symbol(__DEV__ ? `capitalize` : ``) export const TO_HANDLER_KEY = Symbol(__DEV__ ? `toHandlerKey` : ``) export const SET_BLOCK_TRACKING = Symbol(__DEV__ ? `setBlockTracking` : ``) -export const PUSH_SCOPE_ID = Symbol(__DEV__ ? `pushScopeId` : ``) -export const POP_SCOPE_ID = Symbol(__DEV__ ? `popScopeId` : ``) -export const WITH_SCOPE_ID = Symbol(__DEV__ ? `withScopeId` : ``) +export const SET_SCOPE_ID = Symbol(__DEV__ ? `setScopeId` : ``) export const WITH_CTX = Symbol(__DEV__ ? `withCtx` : ``) export const UNREF = Symbol(__DEV__ ? `unref` : ``) export const IS_REF = Symbol(__DEV__ ? `isRef` : ``) @@ -61,9 +59,7 @@ export const helperNameMap: any = { [CAPITALIZE]: `capitalize`, [TO_HANDLER_KEY]: `toHandlerKey`, [SET_BLOCK_TRACKING]: `setBlockTracking`, - [PUSH_SCOPE_ID]: `pushScopeId`, - [POP_SCOPE_ID]: `popScopeId`, - [WITH_SCOPE_ID]: `withScopeId`, + [SET_SCOPE_ID]: `setScopeId`, [WITH_CTX]: `withCtx`, [UNREF]: `unref`, [IS_REF]: `isRef` diff --git a/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts b/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts index 5d7951c1f48..954a7d44fbd 100644 --- a/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts @@ -10,13 +10,11 @@ describe('ssr: scopeId', () => { mode: 'module' }).code ).toMatchInlineSnapshot(` - "import { withScopeId as _withScopeId } from \\"vue\\" - import { ssrRenderAttrs as _ssrRenderAttrs } from \\"@vue/server-renderer\\" - const _withId = /*#__PURE__*/_withScopeId(\\"data-v-xxxxxxx\\") + "import { ssrRenderAttrs as _ssrRenderAttrs } from \\"@vue/server-renderer\\" - export const ssrRender = /*#__PURE__*/_withId((_ctx, _push, _parent, _attrs) => { + export function ssrRender(_ctx, _push, _parent, _attrs) { _push(\`hello
\`) - })" + }" `) }) @@ -28,15 +26,14 @@ describe('ssr: scopeId', () => { mode: 'module' }).code ).toMatchInlineSnapshot(` - "import { resolveComponent as _resolveComponent, withCtx as _withCtx, createTextVNode as _createTextVNode, withScopeId as _withScopeId } from \\"vue\\" + "import { resolveComponent as _resolveComponent, withCtx as _withCtx, createTextVNode as _createTextVNode } from \\"vue\\" import { ssrRenderComponent as _ssrRenderComponent } from \\"@vue/server-renderer\\" - const _withId = /*#__PURE__*/_withScopeId(\\"data-v-xxxxxxx\\") - export const ssrRender = /*#__PURE__*/_withId((_ctx, _push, _parent, _attrs) => { + export function ssrRender(_ctx, _push, _parent, _attrs) { const _component_foo = _resolveComponent(\\"foo\\") _push(_ssrRenderComponent(_component_foo, _attrs, { - default: _withId((_, _push, _parent, _scopeId) => { + default: _withCtx((_, _push, _parent, _scopeId) => { if (_push) { _push(\`foo\`) } else { @@ -47,7 +44,7 @@ describe('ssr: scopeId', () => { }), _: 1 /* STABLE */ }, _parent)) - })" + }" `) }) @@ -58,15 +55,14 @@ describe('ssr: scopeId', () => { mode: 'module' }).code ).toMatchInlineSnapshot(` - "import { resolveComponent as _resolveComponent, withCtx as _withCtx, createVNode as _createVNode, withScopeId as _withScopeId } from \\"vue\\" + "import { resolveComponent as _resolveComponent, withCtx as _withCtx, createVNode as _createVNode } from \\"vue\\" import { ssrRenderComponent as _ssrRenderComponent } from \\"@vue/server-renderer\\" - const _withId = /*#__PURE__*/_withScopeId(\\"data-v-xxxxxxx\\") - export const ssrRender = /*#__PURE__*/_withId((_ctx, _push, _parent, _attrs) => { + export function ssrRender(_ctx, _push, _parent, _attrs) { const _component_foo = _resolveComponent(\\"foo\\") _push(_ssrRenderComponent(_component_foo, _attrs, { - default: _withId((_, _push, _parent, _scopeId) => { + default: _withCtx((_, _push, _parent, _scopeId) => { if (_push) { _push(\`hello\`) } else { @@ -77,7 +73,7 @@ describe('ssr: scopeId', () => { }), _: 1 /* STABLE */ }, _parent)) - })" + }" `) }) @@ -88,20 +84,19 @@ describe('ssr: scopeId', () => { mode: 'module' }).code ).toMatchInlineSnapshot(` - "import { resolveComponent as _resolveComponent, withCtx as _withCtx, createVNode as _createVNode, withScopeId as _withScopeId } from \\"vue\\" + "import { resolveComponent as _resolveComponent, withCtx as _withCtx, createVNode as _createVNode } from \\"vue\\" import { ssrRenderComponent as _ssrRenderComponent } from \\"@vue/server-renderer\\" - const _withId = /*#__PURE__*/_withScopeId(\\"data-v-xxxxxxx\\") - export const ssrRender = /*#__PURE__*/_withId((_ctx, _push, _parent, _attrs) => { + export function ssrRender(_ctx, _push, _parent, _attrs) { const _component_foo = _resolveComponent(\\"foo\\") const _component_bar = _resolveComponent(\\"bar\\") _push(_ssrRenderComponent(_component_foo, _attrs, { - default: _withId((_, _push, _parent, _scopeId) => { + default: _withCtx((_, _push, _parent, _scopeId) => { if (_push) { _push(\`hello\`) _push(_ssrRenderComponent(_component_bar, null, { - default: _withId((_, _push, _parent, _scopeId) => { + default: _withCtx((_, _push, _parent, _scopeId) => { if (_push) { _push(\`\`) } else { @@ -111,12 +106,12 @@ describe('ssr: scopeId', () => { } }), _: 1 /* STABLE */ - }, _parent)) + }, _parent, _scopeId)) } else { return [ _createVNode(\\"span\\", null, \\"hello\\"), _createVNode(_component_bar, null, { - default: _withId(() => [ + default: _withCtx(() => [ _createVNode(\\"span\\") ]), _: 1 /* STABLE */ @@ -126,7 +121,7 @@ describe('ssr: scopeId', () => { }), _: 1 /* STABLE */ }, _parent)) - })" + }" `) }) }) diff --git a/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts b/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts index 668a80e8789..2219ff07725 100644 --- a/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts @@ -6,7 +6,7 @@ describe('ssr: ', () => { "const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\") return function ssrRender(_ctx, _push, _parent, _attrs) { - _ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent) + _ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent, null) }" `) }) @@ -16,7 +16,7 @@ describe('ssr: ', () => { "const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\") return function ssrRender(_ctx, _push, _parent, _attrs) { - _ssrRenderSlot(_ctx.$slots, \\"foo\\", {}, null, _push, _parent) + _ssrRenderSlot(_ctx.$slots, \\"foo\\", {}, null, _push, _parent, null) }" `) }) @@ -26,7 +26,7 @@ describe('ssr: ', () => { "const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\") return function ssrRender(_ctx, _push, _parent, _attrs) { - _ssrRenderSlot(_ctx.$slots, _ctx.bar.baz, {}, null, _push, _parent) + _ssrRenderSlot(_ctx.$slots, _ctx.bar.baz, {}, null, _push, _parent, null) }" `) }) @@ -40,7 +40,7 @@ describe('ssr: ', () => { _ssrRenderSlot(_ctx.$slots, \\"foo\\", { p: 1, bar: \\"2\\" - }, null, _push, _parent) + }, null, _push, _parent, null) }" `) }) @@ -53,7 +53,49 @@ describe('ssr: ', () => { return function ssrRender(_ctx, _push, _parent, _attrs) { _ssrRenderSlot(_ctx.$slots, \\"default\\", {}, () => { _push(\`some \${_ssrInterpolate(_ctx.fallback)} content\`) - }, _push, _parent) + }, _push, _parent, null) + }" + `) + }) + + test('with scopeId', async () => { + expect( + compile(``, { + scopeId: 'hello' + }).code + ).toMatchInlineSnapshot(` + "const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent, \\"hello-s\\") + }" + `) + }) + + test('with forwarded scopeId', async () => { + expect( + compile(``, { + scopeId: 'hello' + }).code + ).toMatchInlineSnapshot(` + "const { resolveComponent: _resolveComponent, withCtx: _withCtx, renderSlot: _renderSlot } = require(\\"vue\\") + const { ssrRenderSlot: _ssrRenderSlot, ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + const _component_Comp = _resolveComponent(\\"Comp\\") + + _push(_ssrRenderComponent(_component_Comp, _attrs, { + default: _withCtx((_, _push, _parent, _scopeId) => { + if (_push) { + _ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent, \\"hello-s\\" + _scopeId) + } else { + return [ + _renderSlot(_ctx.$slots, \\"default\\") + ] + } + }), + _: 3 /* FORWARDED */ + }, _parent)) }" `) }) diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts index 5acf8000b17..de96b7efc67 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts @@ -203,6 +203,12 @@ export function ssrProcessComponent( vnodeBranch ) } + + // component is inside a slot, inherit slot scope Id + if (context.withSlotScopeId) { + node.ssrCodegenNode!.arguments.push(`_scopeId`) + } + if (typeof component === 'string') { // static component context.pushStatement( diff --git a/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts b/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts index b40d17ab478..b2b2de4f5fd 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts @@ -21,9 +21,11 @@ export const ssrTransformSlotOutlet: NodeTransform = (node, context) => { `_ctx.$slots`, slotName, slotProps || `{}`, - `null`, // fallback content placeholder. + // fallback content placeholder. will be replaced in the process phase + `null`, `_push`, - `_parent` + `_parent`, + context.scopeId ? `"${context.scopeId}-s"` : `null` ] ) } @@ -34,6 +36,7 @@ export function ssrProcessSlotOutlet( context: SSRTransformContext ) { const renderCall = node.ssrCodegenNode! + // has fallback content if (node.children.length) { const fallbackRenderFn = createFunctionExpression([]) @@ -41,5 +44,13 @@ export function ssrProcessSlotOutlet( // _renderSlot(slots, name, props, fallback, ...) renderCall.arguments[3] = fallbackRenderFn } + + // Forwarded . Add slot scope id + if (context.withSlotScopeId) { + const scopeId = renderCall.arguments[6] as string + renderCall.arguments[6] = + scopeId === `null` ? `_scopeId` : `${scopeId} + _scopeId` + } + context.pushStatement(node.ssrCodegenNode!) } diff --git a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts index 0b3e7f5938a..1cc7fe01eff 100644 --- a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts @@ -14,8 +14,7 @@ import { ComponentPublicInstance, Ref, cloneVNode, - provide, - withScopeId + provide } from '@vue/runtime-test' import { KeepAliveProps } from '../../src/components/KeepAlive' @@ -804,14 +803,13 @@ describe('KeepAlive', () => { test('should work with cloned root due to scopeId / fallthrough attrs', async () => { const viewRef = ref('one') const instanceRef = ref(null) - const withId = withScopeId('foo') const App = { __scopeId: 'foo', - render: withId(() => { + render: () => { return h(KeepAlive, null, { default: () => h(views[viewRef.value], { ref: instanceRef }) }) - }) + } } render(h(App), root) expect(serializeInner(root)).toBe(`
one
`) diff --git a/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts b/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts index 99f2292b771..0cc8f3babf5 100644 --- a/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts +++ b/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts @@ -8,7 +8,7 @@ import { Fragment, createCommentVNode } from '../../src' -import { PatchFlags } from '@vue/shared/src' +import { PatchFlags } from '@vue/shared' describe('renderSlot', () => { it('should render slot', () => { @@ -37,7 +37,7 @@ describe('renderSlot', () => { return [createVNode('div', null, 'foo', PatchFlags.TEXT)] }, // mock instance - {} as any + { type: {} } as any ) // manual invocation should not track diff --git a/packages/runtime-core/__tests__/helpers/scopeId.spec.ts b/packages/runtime-core/__tests__/helpers/scopeId.spec.ts deleted file mode 100644 index f570c7f0c12..00000000000 --- a/packages/runtime-core/__tests__/helpers/scopeId.spec.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { withScopeId } from '../../src/helpers/scopeId' -import { h, render, nodeOps, serializeInner } from '@vue/runtime-test' - -describe('scopeId runtime support', () => { - const withParentId = withScopeId('parent') - const withChildId = withScopeId('child') - - test('should attach scopeId', () => { - const App = { - __scopeId: 'parent', - render: withParentId(() => { - return h('div', [h('div')]) - }) - } - const root = nodeOps.createElement('div') - render(h(App), root) - expect(serializeInner(root)).toBe(`
`) - }) - - test('should attach scopeId to components in parent component', () => { - const Child = { - __scopeId: 'child', - render: withChildId(() => { - return h('div') - }) - } - const App = { - __scopeId: 'parent', - render: withParentId(() => { - return h('div', [h(Child)]) - }) - } - - const root = nodeOps.createElement('div') - render(h(App), root) - expect(serializeInner(root)).toBe( - `
` - ) - }) - - test('should work on slots', () => { - const Child = { - __scopeId: 'child', - render: withChildId(function(this: any) { - return h('div', this.$slots.default()) - }) - } - const withChild2Id = withScopeId('child2') - const Child2 = { - __scopeId: 'child2', - render: withChild2Id(() => h('span')) - } - const App = { - __scopeId: 'parent', - render: withParentId(() => { - return h( - Child, - withParentId(() => { - return [h('div'), h(Child2)] - }) - ) - }) - } - const root = nodeOps.createElement('div') - render(h(App), root) - // slot content should have: - // - scopeId from parent - // - slotted scopeId (with `-s` postfix) from child (the tree owner) - expect(serializeInner(root)).toBe( - `
` + - `
` + - // component inside slot should have: - // - scopeId from template context - // - slotted scopeId from slot owner - // - its own scopeId - `` + - `
` - ) - }) - - // #1988 - test('should inherit scopeId through nested HOCs with inheritAttrs: false', () => { - const withParentId = withScopeId('parent') - const App = { - __scopeId: 'parent', - render: withParentId(() => { - return h(Child) - }) - } - - function Child() { - return h(Child2, { class: 'foo' }) - } - - function Child2() { - return h('div') - } - Child2.inheritAttrs = false - - const root = nodeOps.createElement('div') - render(h(App), root) - - expect(serializeInner(root)).toBe(`
`) - }) -}) diff --git a/packages/runtime-core/__tests__/scopeId.spec.ts b/packages/runtime-core/__tests__/scopeId.spec.ts new file mode 100644 index 00000000000..e81af6c56fa --- /dev/null +++ b/packages/runtime-core/__tests__/scopeId.spec.ts @@ -0,0 +1,178 @@ +import { + h, + render, + nodeOps, + serializeInner, + renderSlot +} from '@vue/runtime-test' +import { setScopeId, withCtx } from '../src/componentRenderContext' + +describe('scopeId runtime support', () => { + test('should attach scopeId', () => { + const App = { + __scopeId: 'parent', + render: () => h('div', [h('div')]) + } + const root = nodeOps.createElement('div') + render(h(App), root) + expect(serializeInner(root)).toBe(`
`) + }) + + test('should attach scopeId to components in parent component', () => { + const Child = { + __scopeId: 'child', + render: () => h('div') + } + const App = { + __scopeId: 'parent', + render: () => h('div', [h(Child)]) + } + + const root = nodeOps.createElement('div') + render(h(App), root) + expect(serializeInner(root)).toBe( + `
` + ) + }) + + // :slotted basic + test('should work on slots', () => { + const Child = { + __scopeId: 'child', + render(this: any) { + return h('div', renderSlot(this.$slots, 'default')) + } + } + const Child2 = { + __scopeId: 'child2', + render: () => h('span') + } + const App = { + __scopeId: 'parent', + render: () => { + return h( + Child, + withCtx(() => { + return [h('div'), h(Child2)] + }) + ) + } + } + const root = nodeOps.createElement('div') + render(h(App), root) + // slot content should have: + // - scopeId from parent + // - slotted scopeId (with `-s` postfix) from child (the tree owner) + expect(serializeInner(root)).toBe( + `
` + + `
` + + // component inside slot should have: + // - scopeId from template context + // - slotted scopeId from slot owner + // - its own scopeId + `` + + `
` + ) + }) + + // #2892 + test(':slotted on forwarded slots', async () => { + const Wrapper = { + __scopeId: 'wrapper', + render(this: any) { + //
+ return h('div', { class: 'wrapper' }, [ + renderSlot(this.$slots, 'default') + ]) + } + } + + const Slotted = { + __scopeId: 'slotted', + render(this: any) { + // + return h(Wrapper, null, { + default: withCtx(() => [renderSlot(this.$slots, 'default')]) + }) + } + } + + // simulate hoisted node + setScopeId('root') + const hoisted = h('div', 'hoisted') + setScopeId(null) + + const Root = { + __scopeId: 'root', + render(this: any) { + //
hoisted
{{ dynamic }}
+ return h(Slotted, null, { + default: withCtx(() => { + return [hoisted, h('div', 'dynamic')] + }) + }) + } + } + + const root = nodeOps.createElement('div') + render(h(Root), root) + expect(serializeInner(root)).toBe( + `
` + + `
hoisted
` + + `
dynamic
` + + `
` + ) + + const Root2 = { + __scopeId: 'root', + render(this: any) { + // + // + //
hoisted
{{ dynamic }}
+ //
+ //
+ return h(Slotted, null, { + default: withCtx(() => [ + h(Wrapper, null, { + default: withCtx(() => [hoisted, h('div', 'dynamic')]) + }) + ]) + }) + } + } + const root2 = nodeOps.createElement('div') + render(h(Root2), root2) + expect(serializeInner(root2)).toBe( + `
` + + `
` + + `
hoisted
` + + `
dynamic
` + + `
` + + `
` + ) + }) + + // #1988 + test('should inherit scopeId through nested HOCs with inheritAttrs: false', () => { + const App = { + __scopeId: 'parent', + render: () => { + return h(Child) + } + } + + function Child() { + return h(Child2, { class: 'foo' }) + } + + function Child2() { + return h('div') + } + Child2.inheritAttrs = false + + const root = nodeOps.createElement('div') + render(h(App), root) + + expect(serializeInner(root)).toBe(`
`) + }) +}) diff --git a/packages/runtime-core/__tests__/vnode.spec.ts b/packages/runtime-core/__tests__/vnode.spec.ts index 56f1e7b430c..b5a50105317 100644 --- a/packages/runtime-core/__tests__/vnode.spec.ts +++ b/packages/runtime-core/__tests__/vnode.spec.ts @@ -14,7 +14,7 @@ import { Data } from '../src/component' import { ShapeFlags, PatchFlags } from '@vue/shared' import { h, reactive, isReactive, setBlockTracking } from '../src' import { createApp, nodeOps, serializeInner } from '@vue/runtime-test' -import { setCurrentRenderingInstance } from '../src/componentRenderUtils' +import { setCurrentRenderingInstance } from '../src/componentRenderContext' describe('vnode', () => { test('create with just tag', () => { @@ -231,8 +231,8 @@ describe('vnode', () => { // ref normalizes to [currentRenderingInstance, ref] test('cloneVNode ref normalization', () => { - const mockInstance1 = {} as any - const mockInstance2 = {} as any + const mockInstance1 = { type: {} } as any + const mockInstance2 = { type: {} } as any setCurrentRenderingInstance(mockInstance1) const original = createVNode('div', { ref: 'foo' }) @@ -272,8 +272,8 @@ describe('vnode', () => { }) test('cloneVNode ref merging', () => { - const mockInstance1 = {} as any - const mockInstance2 = {} as any + const mockInstance1 = { type: {} } as any + const mockInstance2 = { type: {} } as any setCurrentRenderingInstance(mockInstance1) const original = createVNode('div', { ref: 'foo' }) diff --git a/packages/runtime-core/src/apiInject.ts b/packages/runtime-core/src/apiInject.ts index 402113cd1b1..a1ec6126be6 100644 --- a/packages/runtime-core/src/apiInject.ts +++ b/packages/runtime-core/src/apiInject.ts @@ -1,6 +1,6 @@ import { isFunction } from '@vue/shared' import { currentInstance } from './component' -import { currentRenderingInstance } from './componentRenderUtils' +import { currentRenderingInstance } from './componentRenderContext' import { warn } from './warning' export interface InjectionKey extends Symbol {} diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 9b142036219..bfb7736b410 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -51,10 +51,8 @@ import { } from '@vue/shared' import { SuspenseBoundary } from './components/Suspense' import { CompilerOptions } from '@vue/compiler-core' -import { - currentRenderingInstance, - markAttrsAccessed -} from './componentRenderUtils' +import { markAttrsAccessed } from './componentRenderUtils' +import { currentRenderingInstance } from './componentRenderContext' import { startMeasure, endMeasure } from './profiling' export type Data = Record diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index c493fea0b90..bbdb0336092 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -35,10 +35,8 @@ import { } from './componentOptions' import { EmitsOptions, EmitFn } from './componentEmits' import { Slots } from './componentSlots' -import { - currentRenderingInstance, - markAttrsAccessed -} from './componentRenderUtils' +import { markAttrsAccessed } from './componentRenderUtils' +import { currentRenderingInstance } from './componentRenderContext' import { warn } from './warning' import { UnionToIntersection } from './helpers/typeUtils' diff --git a/packages/runtime-core/src/componentRenderContext.ts b/packages/runtime-core/src/componentRenderContext.ts new file mode 100644 index 00000000000..78297dc5a54 --- /dev/null +++ b/packages/runtime-core/src/componentRenderContext.ts @@ -0,0 +1,57 @@ +import { ComponentInternalInstance } from './component' +import { isRenderingCompiledSlot } from './helpers/renderSlot' +import { closeBlock, openBlock } from './vnode' + +/** + * mark the current rendering instance for asset resolution (e.g. + * resolveComponent, resolveDirective) during render + */ +export let currentRenderingInstance: ComponentInternalInstance | null = null +export let currentScopeId: string | null = null + +export function setCurrentRenderingInstance( + instance: ComponentInternalInstance | null +) { + currentRenderingInstance = instance + currentScopeId = (instance && instance.type.__scopeId) || null +} + +/** + * Set scope id when creating hoisted vnodes. + * @private compiler helper + */ +export function setScopeId(id: string | null) { + currentScopeId = id +} + +/** + * Wrap a slot function to memoize current rendering instance + * @private compiler helper + */ +export function withCtx( + fn: Function, + ctx: ComponentInternalInstance | null = currentRenderingInstance +) { + if (!ctx) return fn + const renderFnWithContext = (...args: any[]) => { + // If a user calls a compiled slot inside a template expression (#1745), it + // can mess up block tracking, so by default we need to push a null block to + // avoid that. This isn't necessary if rendering a compiled ``. + if (!isRenderingCompiledSlot) { + openBlock(true /* null block that disables tracking */) + } + const prevInstance = currentRenderingInstance + setCurrentRenderingInstance(ctx) + const res = fn(...args) + setCurrentRenderingInstance(prevInstance) + if (!isRenderingCompiledSlot) { + closeBlock() + } + return res + } + // mark this as a compiled slot function. + // this is used in vnode.ts -> normalizeChildren() to set the slot + // rendering flag. + renderFnWithContext._c = true + return renderFnWithContext +} diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index c1454e96c1e..89cdcf19c95 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -18,18 +18,7 @@ import { warn } from './warning' import { isHmrUpdating } from './hmr' import { NormalizedProps } from './componentProps' import { isEmitListener } from './componentEmits' - -/** - * mark the current rendering instance for asset resolution (e.g. - * resolveComponent, resolveDirective) during render - */ -export let currentRenderingInstance: ComponentInternalInstance | null = null - -export function setCurrentRenderingInstance( - instance: ComponentInternalInstance | null -) { - currentRenderingInstance = instance -} +import { setCurrentRenderingInstance } from './componentRenderContext' /** * dev only flag to track whether $attrs was used during render. @@ -63,7 +52,7 @@ export function renderComponentRoot( } = instance let result - currentRenderingInstance = instance + setCurrentRenderingInstance(instance) if (__DEV__) { accessedAttrs = false } @@ -215,8 +204,8 @@ export function renderComponentRoot( handleError(err, instance, ErrorCodes.RENDER_FUNCTION) result = createVNode(Comment) } - currentRenderingInstance = null + setCurrentRenderingInstance(null) return result } diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts index 4a6a1b75375..fbb00969747 100644 --- a/packages/runtime-core/src/componentSlots.ts +++ b/packages/runtime-core/src/componentSlots.ts @@ -17,7 +17,7 @@ import { } from '@vue/shared' import { warn } from './warning' import { isKeepAlive } from './components/KeepAlive' -import { withCtx } from './helpers/withRenderContext' +import { withCtx } from './componentRenderContext' import { isHmrUpdating } from './hmr' export type Slot = (...args: any[]) => VNode[] diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index 6ad53fda72e..b6d04cbbac3 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -112,6 +112,7 @@ const KeepAliveImpl = { instance, parentSuspense, isSVG, + vnode.slotScopeIds, optimized ) queuePostRenderEffect(() => { diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 1b5adfed350..0ec78125b20 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -46,6 +46,7 @@ export const SuspenseImpl = { parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean, // platform-specific impl passed from renderer rendererInternals: RendererInternals @@ -58,6 +59,7 @@ export const SuspenseImpl = { parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized, rendererInternals ) @@ -69,6 +71,8 @@ export const SuspenseImpl = { anchor, parentComponent, isSVG, + slotScopeIds, + optimized, rendererInternals ) } @@ -92,6 +96,7 @@ function mountSuspense( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean, rendererInternals: RendererInternals ) { @@ -108,6 +113,7 @@ function mountSuspense( hiddenContainer, anchor, isSVG, + slotScopeIds, optimized, rendererInternals )) @@ -120,7 +126,8 @@ function mountSuspense( null, parentComponent, suspense, - isSVG + isSVG, + slotScopeIds ) // now check if we have encountered any async deps if (suspense.deps > 0) { @@ -133,7 +140,8 @@ function mountSuspense( anchor, parentComponent, null, // fallback tree will not have suspense context - isSVG + isSVG, + slotScopeIds ) setActiveBranch(suspense, vnode.ssFallback!) } else { @@ -149,6 +157,8 @@ function patchSuspense( anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, isSVG: boolean, + slotScopeIds: string[] | null, + optimized: boolean, { p: patch, um: unmount, o: { createElement } }: RendererInternals ) { const suspense = (n2.suspense = n1.suspense)! @@ -169,7 +179,9 @@ function patchSuspense( null, parentComponent, suspense, - isSVG + isSVG, + slotScopeIds, + optimized ) if (suspense.deps <= 0) { suspense.resolve() @@ -181,7 +193,9 @@ function patchSuspense( anchor, parentComponent, null, // fallback tree will not have suspense context - isSVG + isSVG, + slotScopeIds, + optimized ) setActiveBranch(suspense, newFallback) } @@ -214,7 +228,9 @@ function patchSuspense( null, parentComponent, suspense, - isSVG + isSVG, + slotScopeIds, + optimized ) if (suspense.deps <= 0) { suspense.resolve() @@ -226,7 +242,9 @@ function patchSuspense( anchor, parentComponent, null, // fallback tree will not have suspense context - isSVG + isSVG, + slotScopeIds, + optimized ) setActiveBranch(suspense, newFallback) } @@ -239,7 +257,9 @@ function patchSuspense( anchor, parentComponent, suspense, - isSVG + isSVG, + slotScopeIds, + optimized ) // force resolve suspense.resolve(true) @@ -252,7 +272,9 @@ function patchSuspense( null, parentComponent, suspense, - isSVG + isSVG, + slotScopeIds, + optimized ) if (suspense.deps <= 0) { suspense.resolve() @@ -269,7 +291,9 @@ function patchSuspense( anchor, parentComponent, suspense, - isSVG + isSVG, + slotScopeIds, + optimized ) setActiveBranch(suspense, newBranch) } else { @@ -289,7 +313,9 @@ function patchSuspense( null, parentComponent, suspense, - isSVG + isSVG, + slotScopeIds, + optimized ) if (suspense.deps <= 0) { // incoming branch has no async deps, resolve now. @@ -352,6 +378,7 @@ function createSuspenseBoundary( hiddenContainer: RendererElement, anchor: RendererNode | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean, rendererInternals: RendererInternals, isHydrating = false @@ -507,7 +534,9 @@ function createSuspenseBoundary( anchor, parentComponent, null, // fallback tree will not have suspense context - isSVG + isSVG, + slotScopeIds, + optimized ) setActiveBranch(suspense, fallbackVNode) } @@ -632,6 +661,7 @@ function hydrateSuspense( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean, rendererInternals: RendererInternals, hydrateNode: ( @@ -639,6 +669,7 @@ function hydrateSuspense( vnode: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, + slotScopeIds: string[] | null, optimized: boolean ) => Node | null ): Node | null { @@ -651,6 +682,7 @@ function hydrateSuspense( document.createElement('div'), null, isSVG, + slotScopeIds, optimized, rendererInternals, true /* hydrating */ @@ -666,6 +698,7 @@ function hydrateSuspense( (suspense.pendingBranch = vnode.ssContent!), parentComponent, suspense, + slotScopeIds, optimized ) if (suspense.deps === 0) { diff --git a/packages/runtime-core/src/components/Teleport.ts b/packages/runtime-core/src/components/Teleport.ts index fa61c636a88..e75455f96f0 100644 --- a/packages/runtime-core/src/components/Teleport.ts +++ b/packages/runtime-core/src/components/Teleport.ts @@ -71,6 +71,7 @@ export const TeleportImpl = { parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean, internals: RendererInternals ) { @@ -115,6 +116,7 @@ export const TeleportImpl = { parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } @@ -144,7 +146,8 @@ export const TeleportImpl = { currentContainer, parentComponent, parentSuspense, - isSVG + isSVG, + slotScopeIds ) // even in block tree mode we need to make sure all root-level nodes // in the teleport inherit previous DOM references so that they can @@ -158,7 +161,9 @@ export const TeleportImpl = { currentAnchor, parentComponent, parentSuspense, - isSVG + isSVG, + slotScopeIds, + false ) } @@ -283,6 +288,7 @@ function hydrateTeleport( vnode: TeleportVNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, + slotScopeIds: string[] | null, optimized: boolean, { o: { nextSibling, parentNode, querySelector } @@ -293,6 +299,7 @@ function hydrateTeleport( container: Element, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, + slotScopeIds: string[] | null, optimized: boolean ) => Node | null ): Node | null { @@ -313,6 +320,7 @@ function hydrateTeleport( parentNode(node)!, parentComponent, parentSuspense, + slotScopeIds, optimized ) vnode.targetAnchor = targetNode @@ -324,6 +332,7 @@ function hydrateTeleport( target, parentComponent, parentSuspense, + slotScopeIds, optimized ) } diff --git a/packages/runtime-core/src/directives.ts b/packages/runtime-core/src/directives.ts index c909e9a2edd..20f25d03ee9 100644 --- a/packages/runtime-core/src/directives.ts +++ b/packages/runtime-core/src/directives.ts @@ -15,7 +15,7 @@ import { VNode } from './vnode' import { isFunction, EMPTY_OBJ, makeMap } from '@vue/shared' import { warn } from './warning' import { ComponentInternalInstance, Data } from './component' -import { currentRenderingInstance } from './componentRenderUtils' +import { currentRenderingInstance } from './componentRenderContext' import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling' import { ComponentPublicInstance } from './componentPublicInstance' diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts index 420c4ffc102..56cdd3dcd45 100644 --- a/packages/runtime-core/src/helpers/renderSlot.ts +++ b/packages/runtime-core/src/helpers/renderSlot.ts @@ -53,6 +53,10 @@ export function renderSlot( ? PatchFlags.STABLE_FRAGMENT : PatchFlags.BAIL ) + // TODO (optimization) only add slot scope id if :slotted is used + if (rendered.scopeId) { + rendered.slotScopeIds = [rendered.scopeId + '-s'] + } isRenderingCompiledSlot-- return rendered } diff --git a/packages/runtime-core/src/helpers/resolveAssets.ts b/packages/runtime-core/src/helpers/resolveAssets.ts index 986079a9522..1d6a96bf999 100644 --- a/packages/runtime-core/src/helpers/resolveAssets.ts +++ b/packages/runtime-core/src/helpers/resolveAssets.ts @@ -1,10 +1,10 @@ -import { currentRenderingInstance } from '../componentRenderUtils' import { currentInstance, ConcreteComponent, ComponentOptions, getComponentName } from '../component' +import { currentRenderingInstance } from '../componentRenderContext' import { Directive } from '../directives' import { camelize, capitalize, isString } from '@vue/shared' import { warn } from '../warning' diff --git a/packages/runtime-core/src/helpers/scopeId.ts b/packages/runtime-core/src/helpers/scopeId.ts deleted file mode 100644 index fbefe04a499..00000000000 --- a/packages/runtime-core/src/helpers/scopeId.ts +++ /dev/null @@ -1,36 +0,0 @@ -// SFC scoped style ID management. -// These are only used in esm-bundler builds, but since exports cannot be -// conditional, we can only drop inner implementations in non-bundler builds. - -import { withCtx } from './withRenderContext' - -export let currentScopeId: string | null = null -const scopeIdStack: string[] = [] - -/** - * @private - */ -export function pushScopeId(id: string) { - scopeIdStack.push((currentScopeId = id)) -} - -/** - * @private - */ -export function popScopeId() { - scopeIdStack.pop() - currentScopeId = scopeIdStack[scopeIdStack.length - 1] || null -} - -/** - * @private - */ -export function withScopeId(id: string): (fn: T) => T { - return ((fn: Function) => - withCtx(function(this: any) { - pushScopeId(id) - const res = fn.apply(this, arguments) - popScopeId() - return res - })) as any -} diff --git a/packages/runtime-core/src/helpers/withRenderContext.ts b/packages/runtime-core/src/helpers/withRenderContext.ts deleted file mode 100644 index 88a29ae32f1..00000000000 --- a/packages/runtime-core/src/helpers/withRenderContext.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Slot } from '../componentSlots' -import { - setCurrentRenderingInstance, - currentRenderingInstance -} from '../componentRenderUtils' -import { ComponentInternalInstance } from '../component' -import { isRenderingCompiledSlot } from './renderSlot' -import { closeBlock, openBlock } from '../vnode' - -/** - * Wrap a slot function to memoize current rendering instance - * @private - */ -export function withCtx( - fn: Slot, - ctx: ComponentInternalInstance | null = currentRenderingInstance -) { - if (!ctx) return fn - const renderFnWithContext = (...args: any[]) => { - // If a user calls a compiled slot inside a template expression (#1745), it - // can mess up block tracking, so by default we need to push a null block to - // avoid that. This isn't necessary if rendering a compiled ``. - if (!isRenderingCompiledSlot) { - openBlock(true /* null block that disables tracking */) - } - const owner = currentRenderingInstance - setCurrentRenderingInstance(ctx) - const res = fn(...args) - setCurrentRenderingInstance(owner) - if (!isRenderingCompiledSlot) { - closeBlock() - } - return res - } - renderFnWithContext._c = true - return renderFnWithContext -} diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index c7c88eb65de..cad23524eb7 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -63,7 +63,7 @@ export function createHydrationFunctions( return } hasMismatch = false - hydrateNode(container.firstChild!, vnode, null, null) + hydrateNode(container.firstChild!, vnode, null, null, null) flushPostFlushCbs() if (hasMismatch && !__TEST__) { // this error should show up in production @@ -76,6 +76,7 @@ export function createHydrationFunctions( vnode: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, + slotScopeIds: string[] | null, optimized = false ): Node | null => { const isFragmentStart = isComment(node) && node.data === '[' @@ -85,6 +86,7 @@ export function createHydrationFunctions( vnode, parentComponent, parentSuspense, + slotScopeIds, isFragmentStart ) @@ -147,6 +149,7 @@ export function createHydrationFunctions( vnode, parentComponent, parentSuspense, + slotScopeIds, optimized ) } @@ -164,6 +167,7 @@ export function createHydrationFunctions( vnode, parentComponent, parentSuspense, + slotScopeIds, optimized ) } @@ -171,6 +175,7 @@ export function createHydrationFunctions( // when setting up the render effect, if the initial vnode already // has .el set, the component will perform hydration instead of mount // on its sub-tree. + vnode.slotScopeIds = slotScopeIds const container = parentNode(node)! const hydrateComponent = () => { mountComponent( @@ -205,6 +210,7 @@ export function createHydrationFunctions( vnode as TeleportVNode, parentComponent, parentSuspense, + slotScopeIds, optimized, rendererInternals, hydrateChildren @@ -217,6 +223,7 @@ export function createHydrationFunctions( parentComponent, parentSuspense, isSVGContainer(parentNode(node)!), + slotScopeIds, optimized, rendererInternals, hydrateNode @@ -238,6 +245,7 @@ export function createHydrationFunctions( vnode: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, + slotScopeIds: string[] | null, optimized: boolean ) => { optimized = optimized || !!vnode.dynamicChildren @@ -291,6 +299,7 @@ export function createHydrationFunctions( el, parentComponent, parentSuspense, + slotScopeIds, optimized ) let hasWarned = false @@ -330,6 +339,7 @@ export function createHydrationFunctions( container: Element, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, + slotScopeIds: string[] | null, optimized: boolean ): Node | null => { optimized = optimized || !!parentVNode.dynamicChildren @@ -346,6 +356,7 @@ export function createHydrationFunctions( vnode, parentComponent, parentSuspense, + slotScopeIds, optimized ) } else { @@ -365,7 +376,8 @@ export function createHydrationFunctions( null, parentComponent, parentSuspense, - isSVGContainer(container) + isSVGContainer(container), + slotScopeIds ) } } @@ -377,8 +389,16 @@ export function createHydrationFunctions( vnode: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, + slotScopeIds: string[] | null, optimized: boolean ) => { + const { slotScopeIds: fragmentSlotScopeIds } = vnode + if (fragmentSlotScopeIds) { + slotScopeIds = slotScopeIds + ? slotScopeIds.concat(fragmentSlotScopeIds) + : fragmentSlotScopeIds + } + const container = parentNode(node)! const next = hydrateChildren( nextSibling(node)!, @@ -386,6 +406,7 @@ export function createHydrationFunctions( container, parentComponent, parentSuspense, + slotScopeIds, optimized ) if (next && isComment(next) && next.data === ']') { @@ -405,6 +426,7 @@ export function createHydrationFunctions( vnode: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, + slotScopeIds: string[] | null, isFragment: boolean ): Node | null => { hasMismatch = true @@ -446,7 +468,8 @@ export function createHydrationFunctions( next, parentComponent, parentSuspense, - isSVGContainer(container) + isSVGContainer(container), + slotScopeIds ) return next } diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index a24672226db..98ba289f565 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -227,12 +227,11 @@ export { HMRRuntime } from './hmr' // For compiler generated code // should sync with '@vue/compiler-core/src/runtimeConstants.ts' -export { withCtx } from './helpers/withRenderContext' +export { withCtx, setScopeId } from './componentRenderContext' export { renderList } from './helpers/renderList' export { toHandlers } from './helpers/toHandlers' export { renderSlot } from './helpers/renderSlot' export { createSlots } from './helpers/createSlots' -export { pushScopeId, popScopeId, withScopeId } from './helpers/scopeId' export { openBlock, createBlock, @@ -257,10 +256,8 @@ export { transformVNodeArgs } from './vnode' // change without notice between versions. User code should never rely on them. import { createComponentInstance, setupComponent } from './component' -import { - renderComponentRoot, - setCurrentRenderingInstance -} from './componentRenderUtils' +import { renderComponentRoot } from './componentRenderUtils' +import { setCurrentRenderingInstance } from './componentRenderContext' import { isVNode, normalizeVNode } from './vnode' const _ssrUtils = { diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 3089b102796..f2a35794c37 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -177,6 +177,7 @@ type PatchFn = ( parentComponent?: ComponentInternalInstance | null, parentSuspense?: SuspenseBoundary | null, isSVG?: boolean, + slotScopeIds?: string[] | null, optimized?: boolean ) => void @@ -187,6 +188,7 @@ type MountChildrenFn = ( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean, start?: number ) => void @@ -199,7 +201,8 @@ type PatchChildrenFn = ( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, - optimized?: boolean + slotScopeIds: string[] | null, + optimized: boolean ) => void type PatchBlockChildrenFn = ( @@ -208,7 +211,8 @@ type PatchBlockChildrenFn = ( fallbackContainer: RendererElement, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean + isSVG: boolean, + slotScopeIds: string[] | null ) => void type MoveFn = ( @@ -469,6 +473,7 @@ function baseCreateRenderer( parentComponent = null, parentSuspense = null, isSVG = false, + slotScopeIds = null, optimized = false ) => { // patching & not same type, unmount old tree @@ -507,6 +512,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) break @@ -520,6 +526,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } else if (shapeFlag & ShapeFlags.COMPONENT) { @@ -531,6 +538,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } else if (shapeFlag & ShapeFlags.TELEPORT) { @@ -542,6 +550,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized, internals ) @@ -554,6 +563,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized, internals ) @@ -676,6 +686,7 @@ function baseCreateRenderer( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean ) => { isSVG = isSVG || (n2.type as string) === 'svg' @@ -687,10 +698,19 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } else { - patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized) + patchElement( + n1, + n2, + parentComponent, + parentSuspense, + isSVG, + slotScopeIds, + optimized + ) } } @@ -701,19 +721,12 @@ function baseCreateRenderer( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean ) => { let el: RendererElement let vnodeHook: VNodeHook | undefined | null - const { - type, - props, - shapeFlag, - transition, - scopeId, - patchFlag, - dirs - } = vnode + const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode if ( !__DEV__ && vnode.el && @@ -744,6 +757,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG && type !== 'foreignObject', + slotScopeIds, optimized || !!vnode.dynamicChildren ) } @@ -773,7 +787,7 @@ function baseCreateRenderer( } } // scopeId - setScopeId(el, scopeId, vnode, parentComponent) + setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent) } if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { Object.defineProperty(el, '__vnode', { @@ -813,30 +827,32 @@ function baseCreateRenderer( const setScopeId = ( el: RendererElement, - scopeId: string | false | null, vnode: VNode, + scopeId: string | null, + slotScopeIds: string[] | null, parentComponent: ComponentInternalInstance | null ) => { if (scopeId) { hostSetScopeId(el, scopeId) } - if (parentComponent) { - const treeOwnerId = parentComponent.type.__scopeId - // vnode's own scopeId and the current patched component's scopeId is - // different - this is a slot content node. - if (treeOwnerId && treeOwnerId !== scopeId) { - hostSetScopeId(el, treeOwnerId + '-s') + if (slotScopeIds) { + for (let i = 0; i < slotScopeIds.length; i++) { + hostSetScopeId(el, slotScopeIds[i]) } + } + if (parentComponent) { let subTree = parentComponent.subTree - if (__DEV__ && subTree.type === Fragment) { + if (__DEV__ && subTree.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT) { subTree = filterSingleRoot(subTree.children as VNodeArrayChildren) || subTree } if (vnode === subTree) { + const parentVNode = parentComponent.vnode setScopeId( el, - parentComponent.vnode.scopeId, - parentComponent.vnode, + parentVNode, + parentVNode.scopeId, + parentVNode.slotScopeIds, parentComponent.parent ) } @@ -851,6 +867,7 @@ function baseCreateRenderer( parentSuspense, isSVG, optimized, + slotScopeIds, start = 0 ) => { for (let i = start; i < children.length; i++) { @@ -865,7 +882,8 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, - optimized + optimized, + slotScopeIds ) } } @@ -876,6 +894,7 @@ function baseCreateRenderer( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean ) => { const el = (n2.el = n1.el!) @@ -993,7 +1012,8 @@ function baseCreateRenderer( el, parentComponent, parentSuspense, - areChildrenSVG + areChildrenSVG, + slotScopeIds ) if (__DEV__ && parentComponent && parentComponent.type.__hmrId) { traverseStaticChildren(n1, n2) @@ -1007,7 +1027,9 @@ function baseCreateRenderer( null, parentComponent, parentSuspense, - areChildrenSVG + areChildrenSVG, + slotScopeIds, + false ) } @@ -1026,7 +1048,8 @@ function baseCreateRenderer( fallbackContainer, parentComponent, parentSuspense, - isSVG + isSVG, + slotScopeIds ) => { for (let i = 0; i < newChildren.length; i++) { const oldVNode = oldChildren[i] @@ -1054,6 +1077,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, true ) } @@ -1119,16 +1143,24 @@ function baseCreateRenderer( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean ) => { const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))! const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))! - let { patchFlag, dynamicChildren } = n2 + let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2 if (patchFlag > 0) { optimized = true } + // check if this is a slot fragment with :slotted scope ids + if (fragmentSlotScopeIds) { + slotScopeIds = slotScopeIds + ? slotScopeIds.concat(fragmentSlotScopeIds) + : fragmentSlotScopeIds + } + if (__DEV__ && isHmrUpdating) { // HMR updated, force full diff patchFlag = 0 @@ -1149,6 +1181,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } else { @@ -1168,7 +1201,8 @@ function baseCreateRenderer( container, parentComponent, parentSuspense, - isSVG + isSVG, + slotScopeIds ) if (__DEV__ && parentComponent && parentComponent.type.__hmrId) { traverseStaticChildren(n1, n2) @@ -1195,6 +1229,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } @@ -1209,8 +1244,10 @@ function baseCreateRenderer( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean ) => { + n2.slotScopeIds = slotScopeIds if (n1 == null) { if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { ;(parentComponent!.ctx as KeepAliveContext).activate( @@ -1382,7 +1419,8 @@ function baseCreateRenderer( initialVNode.el as Node, subTree, instance, - parentSuspense + parentSuspense, + null ) if (__DEV__) { endMeasure(instance, `hydrate`) @@ -1543,6 +1581,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized = false ) => { const c1 = n1 && n1.children @@ -1563,6 +1602,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) return @@ -1576,6 +1616,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) return @@ -1604,6 +1645,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } else { @@ -1625,6 +1667,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } @@ -1640,6 +1683,7 @@ function baseCreateRenderer( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean ) => { c1 = c1 || EMPTY_ARR @@ -1660,6 +1704,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } @@ -1682,6 +1727,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized, commonLength ) @@ -1697,6 +1743,7 @@ function baseCreateRenderer( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean ) => { let i = 0 @@ -1721,6 +1768,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } else { @@ -1746,6 +1794,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } else { @@ -1776,7 +1825,9 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG + isSVG, + slotScopeIds, + optimized ) i++ } @@ -1878,6 +1929,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) patched++ @@ -1905,7 +1957,9 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG + isSVG, + slotScopeIds, + optimized ) } else if (moved) { // move if: diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 92d7c4f428b..e46c9842619 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -32,9 +32,11 @@ import { import { DirectiveBinding } from './directives' import { TransitionHooks } from './components/BaseTransition' import { warn } from './warning' -import { currentScopeId } from './helpers/scopeId' import { TeleportImpl, isTeleport } from './components/Teleport' -import { currentRenderingInstance } from './componentRenderUtils' +import { + currentRenderingInstance, + currentScopeId +} from './componentRenderContext' import { RendererNode, RendererElement } from './renderer' import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets' import { hmrDirtyComponents } from './hmr' @@ -133,7 +135,18 @@ export interface VNode< props: (VNodeProps & ExtraProps) | null key: string | number | null ref: VNodeNormalizedRef | null - scopeId: string | null // SFC only + /** + * SFC only. This is assigned on vnode creation using currentScopeId + * which is set alongside currentRenderingInstance. + */ + scopeId: string | null + /** + * SFC only. This is assigned to: + * - Slot fragment vnodes with :slotted SFC styles. + * - Component vnodes (during patch/hydration) so that its root node can + * inherit the component's slotScopeIds + */ + slotScopeIds: string[] | null children: VNodeNormalizedChildren component: ComponentInternalInstance | null dirs: DirectiveBinding[] | null @@ -398,6 +411,7 @@ function _createVNode( key: props && normalizeKey(props), ref: props && normalizeRef(props), scopeId: currentScopeId, + slotScopeIds: null, children: null, component: null, suspense: null, @@ -479,6 +493,7 @@ export function cloneVNode( : normalizeRef(extraProps) : ref, scopeId: vnode.scopeId, + slotScopeIds: vnode.slotScopeIds, children: __DEV__ && patchFlag === PatchFlags.HOISTED && isArray(children) ? (children as VNode[]).map(deepCloneVNode) diff --git a/packages/server-renderer/__tests__/render.spec.ts b/packages/server-renderer/__tests__/render.spec.ts index a02bf365cb7..806ca210b23 100644 --- a/packages/server-renderer/__tests__/render.spec.ts +++ b/packages/server-renderer/__tests__/render.spec.ts @@ -2,13 +2,13 @@ import { createApp, h, createCommentVNode, - withScopeId, resolveComponent, ComponentOptions, ref, defineComponent, createTextVNode, - createStaticVNode + createStaticVNode, + withCtx } from 'vue' import { escapeHtml } from '@vue/shared' import { renderToString } from '../src/renderToString' @@ -634,34 +634,32 @@ function testRender(type: string, render: typeof renderToString) { describe('scopeId', () => { // note: here we are only testing scopeId handling for vdom serialization. // compiled srr render functions will include scopeId directly in strings. - const withId = withScopeId('data-v-test') - const withChildId = withScopeId('data-v-child') test('basic', async () => { - expect( - await render( - withId(() => { - return h('div') - })() - ) - ).toBe(`
`) + const Foo = { + __scopeId: 'data-v-test', + render() { + return h('div') + } + } + expect(await render(h(Foo))).toBe(`
`) }) test('with slots', async () => { const Child = { __scopeId: 'data-v-child', - render: withChildId(function(this: any) { + render: function(this: any) { return h('div', this.$slots.default()) - }) + } } const Parent = { __scopeId: 'data-v-test', - render: withId(() => { + render: () => { return h(Child, null, { - default: withId(() => h('span', 'slot')) + default: withCtx(() => h('span', 'slot')) }) - }) + } } expect(await render(h(Parent))).toBe( diff --git a/packages/server-renderer/__tests__/ssrScopeId.spec.ts b/packages/server-renderer/__tests__/ssrScopeId.spec.ts index 8b58fc66b93..7726739e401 100644 --- a/packages/server-renderer/__tests__/ssrScopeId.spec.ts +++ b/packages/server-renderer/__tests__/ssrScopeId.spec.ts @@ -1,11 +1,9 @@ -import { createApp, withScopeId } from 'vue' +import { createApp, mergeProps, withCtx } from 'vue' import { renderToString } from '../src/renderToString' import { ssrRenderComponent, ssrRenderAttrs, ssrRenderSlot } from '../src' -describe('ssr: scoped id on component root', () => { - test('basic', async () => { - const withParentId = withScopeId('parent') - +describe('ssr: scopedId runtime behavior', () => { + test('id on component root', async () => { const Child = { ssrRender: (ctx: any, push: any, parent: any, attrs: any) => { push(`
`) @@ -13,19 +11,19 @@ describe('ssr: scoped id on component root', () => { } const Comp = { - ssrRender: withParentId((ctx: any, push: any, parent: any) => { + __scopeId: 'parent', + ssrRender: (ctx: any, push: any, parent: any) => { push(ssrRenderComponent(Child), null, null, parent) - }) + } } const result = await renderToString(createApp(Comp)) expect(result).toBe(`
`) }) - test('inside slot', async () => { - const withParentId = withScopeId('parent') - + test('id and :slotted on component root', async () => { const Child = { + //
ssrRender: (_: any, push: any, _parent: any, attrs: any) => { push(``) } @@ -34,29 +32,126 @@ describe('ssr: scoped id on component root', () => { const Wrapper = { __scopeId: 'wrapper', ssrRender: (ctx: any, push: any, parent: any) => { - ssrRenderSlot(ctx.$slots, 'default', {}, null, push, parent) + // + ssrRenderSlot( + ctx.$slots, + 'default', + {}, + null, + push, + parent, + 'wrapper-s' + ) } } const Comp = { - ssrRender: withParentId((_: any, push: any, parent: any) => { + __scopeId: 'parent', + ssrRender: (_: any, push: any, parent: any) => { + // push( ssrRenderComponent( Wrapper, null, { - default: withParentId((_: any, push: any, parent: any) => { - push(ssrRenderComponent(Child, null, null, parent)) - }), + default: withCtx( + (_: any, push: any, parent: any, scopeId: string) => { + push(ssrRenderComponent(Child, null, null, parent, scopeId)) + } + ), _: 1 } as any, parent ) ) - }) + } } const result = await renderToString(createApp(Comp)) expect(result).toBe(`
`) }) + + // #2892 + test(':slotted on forwarded slots', async () => { + const Wrapper = { + __scopeId: 'wrapper', + ssrRender: (ctx: any, push: any, parent: any, attrs: any) => { + //
+ push( + `` + ) + ssrRenderSlot( + ctx.$slots, + 'default', + {}, + null, + push, + parent, + 'wrapper-s' + ) + push(``) + } + } + + const Slotted = { + __scopeId: 'slotted', + ssrRender: (ctx: any, push: any, parent: any, attrs: any) => { + // + push( + ssrRenderComponent( + Wrapper, + attrs, + { + default: withCtx( + (_: any, push: any, parent: any, scopeId: string) => { + ssrRenderSlot( + ctx.$slots, + 'default', + {}, + null, + push, + parent, + 'slotted-s' + scopeId + ) + } + ), + _: 1 + } as any, + parent + ) + ) + } + } + + const Root = { + __scopeId: 'root', + //
+ ssrRender: (_: any, push: any, parent: any, attrs: any) => { + push( + ssrRenderComponent( + Slotted, + attrs, + { + default: withCtx( + (_: any, push: any, parent: any, scopeId: string) => { + push(`
`) + } + ), + _: 1 + } as any, + parent + ) + ) + } + } + + const result = await renderToString(createApp(Root)) + expect(result).toBe( + `
` + + `
` + + `
` + ) + }) }) diff --git a/packages/server-renderer/src/helpers/ssrRenderComponent.ts b/packages/server-renderer/src/helpers/ssrRenderComponent.ts index 000f2b482ed..4709f23a674 100644 --- a/packages/server-renderer/src/helpers/ssrRenderComponent.ts +++ b/packages/server-renderer/src/helpers/ssrRenderComponent.ts @@ -6,10 +6,12 @@ export function ssrRenderComponent( comp: Component, props: Props | null = null, children: Slots | SSRSlots | null = null, - parentComponent: ComponentInternalInstance | null = null + parentComponent: ComponentInternalInstance | null = null, + slotScopeId?: string ): SSRBuffer | Promise { return renderComponentVNode( createVNode(comp, props, children), - parentComponent + parentComponent, + slotScopeId ) } diff --git a/packages/server-renderer/src/helpers/ssrRenderSlot.ts b/packages/server-renderer/src/helpers/ssrRenderSlot.ts index 5ee6113a198..3f3589a3b3f 100644 --- a/packages/server-renderer/src/helpers/ssrRenderSlot.ts +++ b/packages/server-renderer/src/helpers/ssrRenderSlot.ts @@ -15,13 +15,13 @@ export function ssrRenderSlot( slotProps: Props, fallbackRenderFn: (() => void) | null, push: PushFn, - parentComponent: ComponentInternalInstance + parentComponent: ComponentInternalInstance, + slotScopeId?: string | null ) { // template-compiled slots are always rendered as fragments push(``) const slotFn = slots[slotName] if (slotFn) { - const scopeId = parentComponent && parentComponent.type.__scopeId const slotBuffer: SSRBufferItem[] = [] const bufferedPush = (item: SSRBufferItem) => { slotBuffer.push(item) @@ -30,7 +30,7 @@ export function ssrRenderSlot( slotProps, bufferedPush, parentComponent, - scopeId ? ` ${scopeId}-s` : `` + slotScopeId ? ' ' + slotScopeId : '' ) if (Array.isArray(ret)) { // normal slot diff --git a/packages/server-renderer/src/render.ts b/packages/server-renderer/src/render.ts index 938a4f83a4f..fd40f3be4ce 100644 --- a/packages/server-renderer/src/render.ts +++ b/packages/server-renderer/src/render.ts @@ -80,7 +80,8 @@ export function createBuffer() { export function renderComponentVNode( vnode: VNode, - parentComponent: ComponentInternalInstance | null = null + parentComponent: ComponentInternalInstance | null = null, + slotScopeId?: string ): SSRBuffer | Promise { const instance = createComponentInstance(vnode, parentComponent, null) const res = setupComponent(instance, true /* isSSR */) @@ -97,14 +98,15 @@ export function renderComponentVNode( warn(`[@vue/server-renderer]: Uncaught error in serverPrefetch:\n`, err) }) } - return p.then(() => renderComponentSubTree(instance)) + return p.then(() => renderComponentSubTree(instance, slotScopeId)) } else { - return renderComponentSubTree(instance) + return renderComponentSubTree(instance, slotScopeId) } } function renderComponentSubTree( - instance: ComponentInternalInstance + instance: ComponentInternalInstance, + slotScopeId?: string ): SSRBuffer | Promise { const comp = instance.type as Component const { getBuffer, push } = createBuffer() @@ -133,13 +135,10 @@ function renderComponentSubTree( // inherited scopeId const scopeId = instance.vnode.scopeId - const treeOwnerId = instance.parent && instance.parent.type.__scopeId - const slotScopeId = - treeOwnerId && treeOwnerId !== scopeId ? treeOwnerId + '-s' : null if (scopeId || slotScopeId) { attrs = { ...attrs } if (scopeId) attrs[scopeId] = '' - if (slotScopeId) attrs[slotScopeId] = '' + if (slotScopeId) attrs[slotScopeId.trim()] = '' } // set current rendering instance for asset resolution From c9b9bfdc3333fc9aa4e144c63f8c01151cd3221e Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 5 Mar 2021 11:10:58 -0500 Subject: [PATCH 2/6] fix(compiler): properly bail stringfication for nested slot elements --- packages/compiler-core/src/transforms/hoistStatic.ts | 7 +++++++ packages/compiler-dom/src/transforms/stringifyStatic.ts | 7 ++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/compiler-core/src/transforms/hoistStatic.ts b/packages/compiler-core/src/transforms/hoistStatic.ts index 004a1e5a116..e528fdae188 100644 --- a/packages/compiler-core/src/transforms/hoistStatic.ts +++ b/packages/compiler-core/src/transforms/hoistStatic.ts @@ -111,7 +111,14 @@ function walk( // walk further if (child.type === NodeTypes.ELEMENT) { + const isComponent = child.tagType === ElementTypes.COMPONENT + if (isComponent) { + context.scopes.vSlot++ + } walk(child, context) + if (isComponent) { + context.scopes.vSlot-- + } } else if (child.type === NodeTypes.FOR) { // Do not hoist v-for single child because it has to be a block walk(child, context, child.children.length === 1) diff --git a/packages/compiler-dom/src/transforms/stringifyStatic.ts b/packages/compiler-dom/src/transforms/stringifyStatic.ts index 94a4e05396c..7e4c66ad8cd 100644 --- a/packages/compiler-dom/src/transforms/stringifyStatic.ts +++ b/packages/compiler-dom/src/transforms/stringifyStatic.ts @@ -60,11 +60,8 @@ type StringifiableNode = PlainElementNode | TextCallNode * This optimization is only performed in Node.js. */ export const stringifyStatic: HoistTransform = (children, context, parent) => { - if ( - parent.type === NodeTypes.ELEMENT && - (parent.tagType === ElementTypes.COMPONENT || - parent.tagType === ElementTypes.TEMPLATE) - ) { + // bail stringification for slot content + if (context.scopes.vSlot > 0) { return } From 8f08bd272f727992157318ec3033f0b1e7b7ecac Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 5 Mar 2021 12:12:49 -0500 Subject: [PATCH 3/6] perf: support only attaching slot scope ids when necessary This is done by adding the `slotted: false` option to: - compiler-dom - compiler-ssr - compiler-sfc (forwarded to template compiler) At runtime, only slotted component will render slot fragments with slot scope Ids. For SSR, only slotted component will add slot scope Ids to rendered slot content. This should improve both runtime performance and reduce SSR rendered markup size. Note: requires SFC tooling (e.g. `vue-loader` and `vite`) to pass on the `slotted` option from the SFC descriptoer to the `compileTemplate` call. --- .../transforms/transformSlotOutlet.spec.ts | 9 +++++ packages/compiler-core/src/options.ts | 6 ++++ packages/compiler-core/src/transform.ts | 2 ++ .../src/transforms/transformSlotOutlet.ts | 10 ++++++ packages/compiler-sfc/__tests__/parse.spec.ts | 16 +++++++++ packages/compiler-sfc/src/compileTemplate.ts | 3 ++ packages/compiler-sfc/src/parse.ts | 10 +++++- .../__tests__/ssrSlotOutlet.spec.ts | 27 ++++++++++---- .../src/transforms/ssrTransformSlotOutlet.ts | 36 +++++++++++-------- .../runtime-core/__tests__/scopeId.spec.ts | 16 +++++---- .../runtime-core/src/helpers/renderSlot.ts | 6 ++-- 11 files changed, 110 insertions(+), 31 deletions(-) diff --git a/packages/compiler-core/__tests__/transforms/transformSlotOutlet.spec.ts b/packages/compiler-core/__tests__/transforms/transformSlotOutlet.spec.ts index 82a5c8e50e1..1a4ea3da8bf 100644 --- a/packages/compiler-core/__tests__/transforms/transformSlotOutlet.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformSlotOutlet.spec.ts @@ -339,6 +339,15 @@ describe('compiler: transform outlets', () => { }) }) + test('slot with slotted: true', async () => { + const ast = parseWithSlots(``, { slotted: true }) + expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({ + type: NodeTypes.JS_CALL_EXPRESSION, + callee: RENDER_SLOT, + arguments: [`$slots`, `"default"`, `{}`, `undefined`, `true`] + }) + }) + test(`error on unexpected custom directive on `, () => { const onError = jest.fn() const source = `` diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts index 2850da196b1..d8d2573a6e8 100644 --- a/packages/compiler-core/src/options.ts +++ b/packages/compiler-core/src/options.ts @@ -199,6 +199,12 @@ export interface TransformOptions extends SharedTransformCodegenOptions { * SFC scoped styles ID */ scopeId?: string | null + /** + * Indicates this SFC template has used :slotted in its styles + * Defaults to `true` for backwards compatibility - SFC tooling should set it + * to `false` if no `:slotted` usage is detected in ``).descriptor + .slotted + ).toBe(false) + expect( + parse(``) + .descriptor.slotted + ).toBe(true) + expect( + parse(``) + .descriptor.slotted + ).toBe(true) + }) + test('error tolerance', () => { const { errors } = parse(`