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)
}