diff --git a/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts b/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts index a2c39f5b534..e14f4b75025 100644 --- a/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts +++ b/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts @@ -1,5 +1,13 @@ import { renderSlot } from '../../src/helpers/renderSlot' -import { h } from '../../src/h' +import { + h, + withCtx, + createVNode, + openBlock, + createBlock, + Fragment +} from '../../src' +import { PatchFlags } from '@vue/shared/src' describe('renderSlot', () => { it('should render slot', () => { @@ -20,4 +28,23 @@ describe('renderSlot', () => { renderSlot({ default: (_a, _b, _c) => [h('child')] }, 'default') expect('SSR-optimized slot function detected').toHaveBeenWarned() }) + + // #1745 + it('should force enable tracking', () => { + const slot = withCtx( + () => { + return [createVNode('div', null, 'foo', PatchFlags.TEXT)] + }, + // mock instance + {} as any + ) + + // manual invocation should not track + const manual = (openBlock(), createBlock(Fragment, null, slot())) + expect(manual.dynamicChildren!.length).toBe(0) + + // renderSlot should track + const templateRendered = renderSlot({ default: slot }, 'default') + expect(templateRendered.dynamicChildren!.length).toBe(1) + }) }) diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts index fd19b3a95ff..0e4f09a3435 100644 --- a/packages/runtime-core/src/helpers/renderSlot.ts +++ b/packages/runtime-core/src/helpers/renderSlot.ts @@ -10,6 +10,8 @@ import { import { PatchFlags, SlotFlags } from '@vue/shared' import { warn } from '../warning' +export let isRenderingTemplateSlot = false + /** * Compiler runtime helper for rendering `` * @private @@ -33,15 +35,20 @@ export function renderSlot( slot = () => [] } - return ( - openBlock(), - createBlock( - Fragment, - { key: props.key }, - slot ? slot(props) : fallback ? fallback() : [], - (slots as RawSlots)._ === SlotFlags.STABLE - ? PatchFlags.STABLE_FRAGMENT - : PatchFlags.BAIL - ) - ) + // a compiled slot disables block tracking by default to avoid manual + // invocation interfering with template-based block tracking, but in + // `renderSlot` we can be sure that it's template-based so we can force + // enable it. + isRenderingTemplateSlot = true + const rendered = (openBlock(), + createBlock( + Fragment, + { key: props.key }, + slot ? slot(props) : fallback ? fallback() : [], + (slots as RawSlots)._ === SlotFlags.STABLE + ? PatchFlags.STABLE_FRAGMENT + : PatchFlags.BAIL + )) + isRenderingTemplateSlot = false + return rendered } diff --git a/packages/runtime-core/src/helpers/withRenderContext.ts b/packages/runtime-core/src/helpers/withRenderContext.ts index a8f326d081e..bf1541fa11a 100644 --- a/packages/runtime-core/src/helpers/withRenderContext.ts +++ b/packages/runtime-core/src/helpers/withRenderContext.ts @@ -4,6 +4,7 @@ import { currentRenderingInstance } from '../componentRenderUtils' import { ComponentInternalInstance } from '../component' +import { setBlockTracking } from '../vnode' /** * Wrap a slot function to memoize current rendering instance @@ -15,10 +16,15 @@ export function withCtx( ) { if (!ctx) return fn return function renderFnWithContext() { + // By default, compiled slots disables block tracking since the user may + // call it inside a template expression (#1745). It should only track when + // it's called by a template ``. + setBlockTracking(-1) const owner = currentRenderingInstance setCurrentRenderingInstance(ctx) const res = fn.apply(null, arguments as any) setCurrentRenderingInstance(owner) + setBlockTracking(1) return res } } diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 353ed863b05..e2bb819528e 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -35,6 +35,7 @@ import { currentRenderingInstance } from './componentRenderUtils' import { RendererNode, RendererElement } from './renderer' import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets' import { hmrDirtyComponents } from './hmr' +import { isRenderingTemplateSlot } from './helpers/renderSlot' export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as { __isFragment: true @@ -400,18 +401,20 @@ function _createVNode( normalizeChildren(vnode, children) - // presence of a patch flag indicates this node needs patching on updates. - // component nodes also should always be patched, because even if the - // component doesn't need to update, it needs to persist the instance on to - // the next vnode so that it can be properly unmounted later. if ( - shouldTrack > 0 && + (shouldTrack > 0 || isRenderingTemplateSlot) && + // avoid a block node from tracking itself !isBlockNode && + // has current parent block currentBlock && + // presence of a patch flag indicates this node needs patching on updates. + // component nodes also should always be patched, because even if the + // component doesn't need to update, it needs to persist the instance on to + // the next vnode so that it can be properly unmounted later. + (patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) && // the EVENTS flag is only for hydration and if it is the only flag, the // vnode should not be considered dynamic due to handler caching. - patchFlag !== PatchFlags.HYDRATE_EVENTS && - (patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) + patchFlag !== PatchFlags.HYDRATE_EVENTS ) { currentBlock.push(vnode) }