diff --git a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts index 26fee92f..73e566c9 100644 --- a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts +++ b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts @@ -11,9 +11,12 @@ import { serializeInner as inner, VNode, ref, - nextTick + nextTick, + defineComponent, + withCtx, + renderSlot } from '@vue/runtime-test' -import { PatchFlags } from '@vue/shared' +import { PatchFlags, SlotFlags } from '@vue/shared' describe('renderer: optimized mode', () => { let root: TestElement @@ -398,4 +401,52 @@ describe('renderer: optimized mode', () => { expect(inner(root)).toBe('
bar
') expect(block!.dynamicChildren).toBe(null) }) + + // #1980 + test('dynamicChildren should be tracked correctly when normalizing slots to plain children', async () => { + let block: VNode + const Comp = defineComponent({ + setup(_props, { slots }) { + return () => { + const vnode = (openBlock(), + (block = createBlock('div', null, { + default: withCtx(() => [renderSlot(slots, 'default')]), + _: SlotFlags.FORWARDED + }))) + + return vnode + } + } + }) + + const foo = ref(0) + const App = { + setup() { + return () => { + return createVNode(Comp, null, { + default: withCtx(() => [ + createVNode('p', null, foo.value, PatchFlags.TEXT) + ]), + // Indicates that this is a stable slot to avoid bail out + _: SlotFlags.STABLE + }) + } + } + } + + render(h(App), root) + expect(inner(root)).toBe('

0

') + expect(block!.dynamicChildren!.length).toBe(1) + expect(block!.dynamicChildren![0].type).toBe(Fragment) + expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(1) + expect( + serialize(block!.dynamicChildren![0].dynamicChildren![0] + .el as TestElement) + ).toBe('

0

') + + foo.value++ + await nextTick() + + expect(inner(root)).toBe('

1

') + }) }) diff --git a/packages/runtime-core/__tests__/vnode.spec.ts b/packages/runtime-core/__tests__/vnode.spec.ts index ad357561..2ee1872a 100644 --- a/packages/runtime-core/__tests__/vnode.spec.ts +++ b/packages/runtime-core/__tests__/vnode.spec.ts @@ -130,10 +130,10 @@ describe('vnode', () => { }) test('object', () => { - const vnode = createVNode('p', null, { foo: 'foo' }) + const vnode = createVNode({}, null, { foo: 'foo' }) expect(vnode.children).toMatchObject({ foo: 'foo' }) expect(vnode.shapeFlag).toBe( - ShapeFlags.ELEMENT | ShapeFlags.SLOTS_CHILDREN + ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.SLOTS_CHILDREN ) }) diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts index be6dbba4..2e43c628 100644 --- a/packages/runtime-core/src/helpers/renderSlot.ts +++ b/packages/runtime-core/src/helpers/renderSlot.ts @@ -11,6 +11,8 @@ import { PatchFlags, SlotFlags } from '@vue/shared' import { warn } from '../warning' export let isRenderingCompiledSlot = 0 +export const setCompiledSlotRendering = (n: number) => + (isRenderingCompiledSlot += n) /** * Compiler runtime helper for rendering `` diff --git a/packages/runtime-core/src/helpers/withRenderContext.ts b/packages/runtime-core/src/helpers/withRenderContext.ts index 4ac273f5..88a29ae3 100644 --- a/packages/runtime-core/src/helpers/withRenderContext.ts +++ b/packages/runtime-core/src/helpers/withRenderContext.ts @@ -16,7 +16,7 @@ export function withCtx( ctx: ComponentInternalInstance | null = currentRenderingInstance ) { if (!ctx) return fn - return function renderFnWithContext() { + 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 ``. @@ -25,11 +25,13 @@ export function withCtx( } const owner = currentRenderingInstance setCurrentRenderingInstance(ctx) - const res = fn.apply(null, arguments as any) + const res = fn(...args) setCurrentRenderingInstance(owner) if (!isRenderingCompiledSlot) { closeBlock() } return res } + renderFnWithContext._c = true + return renderFnWithContext } diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 0f20a534..de329ff9 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -36,6 +36,7 @@ import { currentRenderingInstance } from './componentRenderUtils' import { RendererNode, RendererElement } from './renderer' import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets' import { hmrDirtyComponents } from './hmr' +import { setCompiledSlotRendering } from './helpers/renderSlot' export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as { __isFragment: true @@ -539,12 +540,15 @@ export function normalizeChildren(vnode: VNode, children: unknown) { } else if (isArray(children)) { type = ShapeFlags.ARRAY_CHILDREN } else if (typeof children === 'object') { - // Normalize slot to plain children - if ( - (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) && - (children as any).default - ) { - normalizeChildren(vnode, (children as any).default()) + if (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) { + // Normalize slot to plain children for plain element and Teleport + const slot = (children as any).default + if (slot) { + // _c marker is added by withCtx() indicating this is a compiled slot + slot._c && setCompiledSlotRendering(1) + normalizeChildren(vnode, slot()) + slot._c && setCompiledSlotRendering(-1) + } return } else { type = ShapeFlags.SLOTS_CHILDREN