diff --git a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
index 3c653281f..82f6f02fb 100644
--- a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
+++ b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
@@ -2,6 +2,7 @@ import {
h,
Fragment,
createVNode,
+ createCommentVNode,
openBlock,
createBlock,
render,
@@ -576,4 +577,119 @@ describe('renderer: optimized mode', () => {
await nextTick()
expect(inner(root)).toBe('
World
')
})
+
+ // #3548
+ test('should not track dynamic children when the user calls a compiled slot inside template expression', () => {
+ const Comp = {
+ setup(props: any, { slots }: SetupContext) {
+ return () => {
+ return (
+ openBlock(),
+ (block = createBlock('section', null, [
+ renderSlot(slots, 'default')
+ ]))
+ )
+ }
+ }
+ }
+
+ let dynamicVNode: VNode
+ const Wrapper = {
+ setup(props: any, { slots }: SetupContext) {
+ return () => {
+ return (
+ openBlock(),
+ createBlock(Comp, null, {
+ default: withCtx(() => {
+ return [
+ (dynamicVNode = createVNode(
+ 'div',
+ {
+ class: {
+ foo: !!slots.default!()
+ }
+ },
+ null,
+ PatchFlags.CLASS
+ ))
+ ]
+ }),
+ _: 1
+ })
+ )
+ }
+ }
+ }
+ const app = createApp({
+ render() {
+ return (
+ openBlock(),
+ createBlock(Wrapper, null, {
+ default: withCtx(() => {
+ return [createVNode({}) /* component */]
+ }),
+ _: 1
+ })
+ )
+ }
+ })
+
+ app.mount(root)
+ expect(inner(root)).toBe('')
+ /**
+ * Block Tree:
+ * - block(div)
+ * - block(Fragment): renderSlots()
+ * - dynamicVNode
+ */
+ expect(block!.dynamicChildren!.length).toBe(1)
+ expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(1)
+ expect(block!.dynamicChildren![0].dynamicChildren![0]).toEqual(
+ dynamicVNode!
+ )
+ })
+
+ // 3569
+ test('should force bailout when the user manually calls the slot function', async () => {
+ const index = ref(0)
+ const Foo = {
+ setup(props: any, { slots }: SetupContext) {
+ return () => {
+ return slots.default!()[index.value]
+ }
+ }
+ }
+
+ const app = createApp({
+ setup() {
+ return () => {
+ return (
+ openBlock(),
+ createBlock(Foo, null, {
+ default: withCtx(() => [
+ true
+ ? (openBlock(), createBlock('p', { key: 0 }, '1'))
+ : createCommentVNode('v-if', true),
+ true
+ ? (openBlock(), createBlock('p', { key: 0 }, '2'))
+ : createCommentVNode('v-if', true)
+ ]),
+ _: 1 /* STABLE */
+ })
+ )
+ }
+ }
+ })
+
+ app.mount(root)
+ expect(inner(root)).toBe('1
')
+
+ index.value = 1
+ await nextTick()
+ expect(inner(root)).toBe('2
')
+
+ index.value = 0
+ await nextTick()
+ expect(inner(root)).toBe('1
')
+ })
})
diff --git a/packages/runtime-core/src/compat/instance.ts b/packages/runtime-core/src/compat/instance.ts
index 5ed4e19f0..966a9a1a6 100644
--- a/packages/runtime-core/src/compat/instance.ts
+++ b/packages/runtime-core/src/compat/instance.ts
@@ -37,6 +37,7 @@ import {
import { resolveFilter } from '../helpers/resolveAssets'
import { resolveMergedOptions } from '../componentOptions'
import { InternalSlots, Slots } from '../componentSlots'
+import { ContextualRenderFn } from '../componentRenderContext'
export type LegacyPublicInstance = ComponentPublicInstance &
LegacyPublicProperties
@@ -106,7 +107,7 @@ export function installCompatInstanceProperties(map: PublicPropertiesMap) {
const res: InternalSlots = {}
for (const key in i.slots) {
const fn = i.slots[key]!
- if (!(fn as any)._nonScoped) {
+ if (!(fn as ContextualRenderFn)._ns /* non-scoped slot */) {
res[key] = fn
}
}
diff --git a/packages/runtime-core/src/compat/renderFn.ts b/packages/runtime-core/src/compat/renderFn.ts
index c65cc8d79..480bf029a 100644
--- a/packages/runtime-core/src/compat/renderFn.ts
+++ b/packages/runtime-core/src/compat/renderFn.ts
@@ -281,7 +281,7 @@ function convertLegacySlots(vnode: VNode): VNode {
for (const key in slots) {
const slotChildren = slots[key]
slots[key] = () => slotChildren
- slots[key]._nonScoped = true
+ slots[key]._ns = true /* non-scoped slot */
}
}
}
diff --git a/packages/runtime-core/src/componentRenderContext.ts b/packages/runtime-core/src/componentRenderContext.ts
index a25756864..8f49ec251 100644
--- a/packages/runtime-core/src/componentRenderContext.ts
+++ b/packages/runtime-core/src/componentRenderContext.ts
@@ -1,7 +1,6 @@
import { ComponentInternalInstance } from './component'
import { devtoolsComponentUpdated } from './devtools'
-import { isRenderingCompiledSlot } from './helpers/renderSlot'
-import { closeBlock, openBlock } from './vnode'
+import { setBlockTracking } from './vnode'
/**
* mark the current rendering instance for asset resolution (e.g.
@@ -56,6 +55,14 @@ export function popScopeId() {
*/
export const withScopeId = (_id: string) => withCtx
+export type ContextualRenderFn = {
+ (...args: any[]): any
+ _n: boolean /* already normalized */
+ _c: boolean /* compiled */
+ _d: boolean /* disableTracking */
+ _ns: boolean /* nonScoped */
+}
+
/**
* Wrap a slot function to memoize current rendering instance
* @private compiler helper
@@ -66,18 +73,26 @@ export function withCtx(
isNonScopedSlot?: boolean // __COMPAT__ only
) {
if (!ctx) return fn
- const renderFnWithContext = (...args: any[]) => {
+
+ // already normalized
+ if ((fn as ContextualRenderFn)._n) {
+ return fn
+ }
+
+ const renderFnWithContext: ContextualRenderFn = (...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 */)
+ // can mess up block tracking, so by default we disable block tracking and
+ // force bail out when invoking a compiled slot (indicated by the ._d flag).
+ // This isn't necessary if rendering a compiled ``, so we flip the
+ // ._d flag off when invoking the wrapped fn inside `renderSlot`.
+ if (renderFnWithContext._d) {
+ setBlockTracking(-1)
}
const prevInstance = setCurrentRenderingInstance(ctx)
const res = fn(...args)
setCurrentRenderingInstance(prevInstance)
- if (!isRenderingCompiledSlot) {
- closeBlock()
+ if (renderFnWithContext._d) {
+ setBlockTracking(1)
}
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
@@ -86,13 +101,18 @@ export function withCtx(
return res
}
- // mark this as a compiled slot function.
+
+ // mark normalized to avoid duplicated wrapping
+ renderFnWithContext._n = true
+ // mark this as compiled by default
// this is used in vnode.ts -> normalizeChildren() to set the slot
// rendering flag.
- // also used to cache the normalized results to avoid repeated normalization
- renderFnWithContext._c = renderFnWithContext
+ renderFnWithContext._c = true
+ // disable block tracking by default
+ renderFnWithContext._d = true
+ // compat build only flag to distinguish scoped slots from non-scoped ones
if (__COMPAT__ && isNonScopedSlot) {
- renderFnWithContext._nonScoped = true
+ renderFnWithContext._ns = true
}
return renderFnWithContext
}
diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts
index 7e0fe3cb6..8c19cec3f 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 './componentRenderContext'
+import { ContextualRenderFn, withCtx } from './componentRenderContext'
import { isHmrUpdating } from './hmr'
import { DeprecationTypes, isCompatEnabled } from './compat/compatConfig'
import { toRaw } from '@vue/reactivity'
@@ -62,9 +62,8 @@ const normalizeSlot = (
key: string,
rawSlot: Function,
ctx: ComponentInternalInstance | null | undefined
-): Slot =>
- (rawSlot as any)._c ||
- (withCtx((props: any) => {
+): Slot => {
+ const normalized = withCtx((props: any) => {
if (__DEV__ && currentInstance) {
warn(
`Slot "${key}" invoked outside of the render function: ` +
@@ -73,7 +72,11 @@ const normalizeSlot = (
)
}
return normalizeSlotValue(rawSlot(props))
- }, ctx) as Slot)
+ }, ctx) as Slot
+ // NOT a compiled slot
+ ;(normalized as ContextualRenderFn)._c = false
+ return normalized
+}
const normalizeObjectSlots = (
rawSlots: RawSlots,
diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts
index 26e7b8250..181d49a54 100644
--- a/packages/runtime-core/src/helpers/renderSlot.ts
+++ b/packages/runtime-core/src/helpers/renderSlot.ts
@@ -1,5 +1,6 @@
import { Data } from '../component'
import { Slots, RawSlots } from '../componentSlots'
+import { ContextualRenderFn } from '../componentRenderContext'
import { Comment, isVNode } from '../vnode'
import {
VNodeArrayChildren,
@@ -11,10 +12,6 @@ import {
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 ``
* @private
@@ -43,7 +40,9 @@ export function renderSlot(
// 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.
- isRenderingCompiledSlot++
+ if (slot && (slot as ContextualRenderFn)._c) {
+ ;(slot as ContextualRenderFn)._d = false
+ }
openBlock()
const validSlotContent = slot && ensureValidVNode(slot(props))
const rendered = createBlock(
@@ -57,7 +56,9 @@ export function renderSlot(
if (!noSlotted && rendered.scopeId) {
rendered.slotScopeIds = [rendered.scopeId + '-s']
}
- isRenderingCompiledSlot--
+ if (slot && (slot as ContextualRenderFn)._c) {
+ ;(slot as ContextualRenderFn)._d = true
+ }
return rendered
}
diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts
index 930db7718..7e5976e2f 100644
--- a/packages/runtime-core/src/vnode.ts
+++ b/packages/runtime-core/src/vnode.ts
@@ -40,7 +40,6 @@ import {
import { RendererNode, RendererElement } from './renderer'
import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets'
import { hmrDirtyComponents } from './hmr'
-import { setCompiledSlotRendering } from './helpers/renderSlot'
import { convertLegacyComponent } from './compat/component'
import { convertLegacyVModelProps } from './compat/componentVModel'
import { defineLegacyVNodeProperties } from './compat/renderFn'
@@ -218,7 +217,7 @@ export function closeBlock() {
// Only tracks when this value is > 0
// We are not using a simple boolean because this value may need to be
// incremented/decremented by nested usage of v-once (see below)
-let shouldTrack = 1
+let isBlockTreeEnabled = 1
/**
* Block tracking sometimes needs to be disabled, for example during the
@@ -237,7 +236,7 @@ let shouldTrack = 1
* @private
*/
export function setBlockTracking(value: number) {
- shouldTrack += value
+ isBlockTreeEnabled += value
}
/**
@@ -263,12 +262,13 @@ export function createBlock(
true /* isBlock: prevent a block from tracking itself */
)
// save current block children on the block vnode
- vnode.dynamicChildren = currentBlock || (EMPTY_ARR as any)
+ vnode.dynamicChildren =
+ isBlockTreeEnabled > 0 ? currentBlock || (EMPTY_ARR as any) : null
// close block
closeBlock()
// a block is always going to be patched, so track it as a child of its
// parent block
- if (shouldTrack > 0 && currentBlock) {
+ if (isBlockTreeEnabled > 0 && currentBlock) {
currentBlock.push(vnode)
}
return vnode
@@ -458,7 +458,7 @@ function _createVNode(
}
if (
- shouldTrack > 0 &&
+ isBlockTreeEnabled > 0 &&
// avoid a block node from tracking itself
!isBlockNode &&
// has current parent block
@@ -635,9 +635,9 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
const slot = (children as any).default
if (slot) {
// _c marker is added by withCtx() indicating this is a compiled slot
- slot._c && setCompiledSlotRendering(1)
+ slot._c && (slot._d = false)
normalizeChildren(vnode, slot())
- slot._c && setCompiledSlotRendering(-1)
+ slot._c && (slot._d = true)
}
return
} else {