Skip to content

Commit 2010607

Browse files
authored
fix(runtime-core): should not track dynamic children when the user calls a compiled slot inside template expression (#3554)
fix #3548, partial fix for #3569
1 parent 1526f94 commit 2010607

File tree

7 files changed

+175
-34
lines changed

7 files changed

+175
-34
lines changed

packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts

+116
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
h,
33
Fragment,
44
createVNode,
5+
createCommentVNode,
56
openBlock,
67
createBlock,
78
render,
@@ -576,4 +577,119 @@ describe('renderer: optimized mode', () => {
576577
await nextTick()
577578
expect(inner(root)).toBe('<div>World</div>')
578579
})
580+
581+
// #3548
582+
test('should not track dynamic children when the user calls a compiled slot inside template expression', () => {
583+
const Comp = {
584+
setup(props: any, { slots }: SetupContext) {
585+
return () => {
586+
return (
587+
openBlock(),
588+
(block = createBlock('section', null, [
589+
renderSlot(slots, 'default')
590+
]))
591+
)
592+
}
593+
}
594+
}
595+
596+
let dynamicVNode: VNode
597+
const Wrapper = {
598+
setup(props: any, { slots }: SetupContext) {
599+
return () => {
600+
return (
601+
openBlock(),
602+
createBlock(Comp, null, {
603+
default: withCtx(() => {
604+
return [
605+
(dynamicVNode = createVNode(
606+
'div',
607+
{
608+
class: {
609+
foo: !!slots.default!()
610+
}
611+
},
612+
null,
613+
PatchFlags.CLASS
614+
))
615+
]
616+
}),
617+
_: 1
618+
})
619+
)
620+
}
621+
}
622+
}
623+
const app = createApp({
624+
render() {
625+
return (
626+
openBlock(),
627+
createBlock(Wrapper, null, {
628+
default: withCtx(() => {
629+
return [createVNode({}) /* component */]
630+
}),
631+
_: 1
632+
})
633+
)
634+
}
635+
})
636+
637+
app.mount(root)
638+
expect(inner(root)).toBe('<section><div class="foo"></div></section>')
639+
/**
640+
* Block Tree:
641+
* - block(div)
642+
* - block(Fragment): renderSlots()
643+
* - dynamicVNode
644+
*/
645+
expect(block!.dynamicChildren!.length).toBe(1)
646+
expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(1)
647+
expect(block!.dynamicChildren![0].dynamicChildren![0]).toEqual(
648+
dynamicVNode!
649+
)
650+
})
651+
652+
// 3569
653+
test('should force bailout when the user manually calls the slot function', async () => {
654+
const index = ref(0)
655+
const Foo = {
656+
setup(props: any, { slots }: SetupContext) {
657+
return () => {
658+
return slots.default!()[index.value]
659+
}
660+
}
661+
}
662+
663+
const app = createApp({
664+
setup() {
665+
return () => {
666+
return (
667+
openBlock(),
668+
createBlock(Foo, null, {
669+
default: withCtx(() => [
670+
true
671+
? (openBlock(), createBlock('p', { key: 0 }, '1'))
672+
: createCommentVNode('v-if', true),
673+
true
674+
? (openBlock(), createBlock('p', { key: 0 }, '2'))
675+
: createCommentVNode('v-if', true)
676+
]),
677+
_: 1 /* STABLE */
678+
})
679+
)
680+
}
681+
}
682+
})
683+
684+
app.mount(root)
685+
expect(inner(root)).toBe('<p>1</p>')
686+
687+
index.value = 1
688+
await nextTick()
689+
expect(inner(root)).toBe('<p>2</p>')
690+
691+
index.value = 0
692+
await nextTick()
693+
expect(inner(root)).toBe('<p>1</p>')
694+
})
579695
})

packages/runtime-core/src/compat/instance.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
import { resolveFilter } from '../helpers/resolveAssets'
3838
import { resolveMergedOptions } from '../componentOptions'
3939
import { InternalSlots, Slots } from '../componentSlots'
40+
import { ContextualRenderFn } from '../componentRenderContext'
4041

4142
export type LegacyPublicInstance = ComponentPublicInstance &
4243
LegacyPublicProperties
@@ -106,7 +107,7 @@ export function installCompatInstanceProperties(map: PublicPropertiesMap) {
106107
const res: InternalSlots = {}
107108
for (const key in i.slots) {
108109
const fn = i.slots[key]!
109-
if (!(fn as any)._nonScoped) {
110+
if (!(fn as ContextualRenderFn)._ns /* non-scoped slot */) {
110111
res[key] = fn
111112
}
112113
}

packages/runtime-core/src/compat/renderFn.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ function convertLegacySlots(vnode: VNode): VNode {
281281
for (const key in slots) {
282282
const slotChildren = slots[key]
283283
slots[key] = () => slotChildren
284-
slots[key]._nonScoped = true
284+
slots[key]._ns = true /* non-scoped slot */
285285
}
286286
}
287287
}

packages/runtime-core/src/componentRenderContext.ts

+33-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { ComponentInternalInstance } from './component'
22
import { devtoolsComponentUpdated } from './devtools'
3-
import { isRenderingCompiledSlot } from './helpers/renderSlot'
4-
import { closeBlock, openBlock } from './vnode'
3+
import { setBlockTracking } from './vnode'
54

65
/**
76
* mark the current rendering instance for asset resolution (e.g.
@@ -56,6 +55,14 @@ export function popScopeId() {
5655
*/
5756
export const withScopeId = (_id: string) => withCtx
5857

58+
export type ContextualRenderFn = {
59+
(...args: any[]): any
60+
_n: boolean /* already normalized */
61+
_c: boolean /* compiled */
62+
_d: boolean /* disableTracking */
63+
_ns: boolean /* nonScoped */
64+
}
65+
5966
/**
6067
* Wrap a slot function to memoize current rendering instance
6168
* @private compiler helper
@@ -66,18 +73,26 @@ export function withCtx(
6673
isNonScopedSlot?: boolean // __COMPAT__ only
6774
) {
6875
if (!ctx) return fn
69-
const renderFnWithContext = (...args: any[]) => {
76+
77+
// already normalized
78+
if ((fn as ContextualRenderFn)._n) {
79+
return fn
80+
}
81+
82+
const renderFnWithContext: ContextualRenderFn = (...args: any[]) => {
7083
// If a user calls a compiled slot inside a template expression (#1745), it
71-
// can mess up block tracking, so by default we need to push a null block to
72-
// avoid that. This isn't necessary if rendering a compiled `<slot>`.
73-
if (!isRenderingCompiledSlot) {
74-
openBlock(true /* null block that disables tracking */)
84+
// can mess up block tracking, so by default we disable block tracking and
85+
// force bail out when invoking a compiled slot (indicated by the ._d flag).
86+
// This isn't necessary if rendering a compiled `<slot>`, so we flip the
87+
// ._d flag off when invoking the wrapped fn inside `renderSlot`.
88+
if (renderFnWithContext._d) {
89+
setBlockTracking(-1)
7590
}
7691
const prevInstance = setCurrentRenderingInstance(ctx)
7792
const res = fn(...args)
7893
setCurrentRenderingInstance(prevInstance)
79-
if (!isRenderingCompiledSlot) {
80-
closeBlock()
94+
if (renderFnWithContext._d) {
95+
setBlockTracking(1)
8196
}
8297

8398
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
@@ -86,13 +101,18 @@ export function withCtx(
86101

87102
return res
88103
}
89-
// mark this as a compiled slot function.
104+
105+
// mark normalized to avoid duplicated wrapping
106+
renderFnWithContext._n = true
107+
// mark this as compiled by default
90108
// this is used in vnode.ts -> normalizeChildren() to set the slot
91109
// rendering flag.
92-
// also used to cache the normalized results to avoid repeated normalization
93-
renderFnWithContext._c = renderFnWithContext
110+
renderFnWithContext._c = true
111+
// disable block tracking by default
112+
renderFnWithContext._d = true
113+
// compat build only flag to distinguish scoped slots from non-scoped ones
94114
if (__COMPAT__ && isNonScopedSlot) {
95-
renderFnWithContext._nonScoped = true
115+
renderFnWithContext._ns = true
96116
}
97117
return renderFnWithContext
98118
}

packages/runtime-core/src/componentSlots.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
} from '@vue/shared'
1818
import { warn } from './warning'
1919
import { isKeepAlive } from './components/KeepAlive'
20-
import { withCtx } from './componentRenderContext'
20+
import { ContextualRenderFn, withCtx } from './componentRenderContext'
2121
import { isHmrUpdating } from './hmr'
2222
import { DeprecationTypes, isCompatEnabled } from './compat/compatConfig'
2323
import { toRaw } from '@vue/reactivity'
@@ -62,9 +62,8 @@ const normalizeSlot = (
6262
key: string,
6363
rawSlot: Function,
6464
ctx: ComponentInternalInstance | null | undefined
65-
): Slot =>
66-
(rawSlot as any)._c ||
67-
(withCtx((props: any) => {
65+
): Slot => {
66+
const normalized = withCtx((props: any) => {
6867
if (__DEV__ && currentInstance) {
6968
warn(
7069
`Slot "${key}" invoked outside of the render function: ` +
@@ -73,7 +72,11 @@ const normalizeSlot = (
7372
)
7473
}
7574
return normalizeSlotValue(rawSlot(props))
76-
}, ctx) as Slot)
75+
}, ctx) as Slot
76+
// NOT a compiled slot
77+
;(normalized as ContextualRenderFn)._c = false
78+
return normalized
79+
}
7780

7881
const normalizeObjectSlots = (
7982
rawSlots: RawSlots,

packages/runtime-core/src/helpers/renderSlot.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Data } from '../component'
22
import { Slots, RawSlots } from '../componentSlots'
3+
import { ContextualRenderFn } from '../componentRenderContext'
34
import { Comment, isVNode } from '../vnode'
45
import {
56
VNodeArrayChildren,
@@ -11,10 +12,6 @@ import {
1112
import { PatchFlags, SlotFlags } from '@vue/shared'
1213
import { warn } from '../warning'
1314

14-
export let isRenderingCompiledSlot = 0
15-
export const setCompiledSlotRendering = (n: number) =>
16-
(isRenderingCompiledSlot += n)
17-
1815
/**
1916
* Compiler runtime helper for rendering `<slot/>`
2017
* @private
@@ -43,7 +40,9 @@ export function renderSlot(
4340
// invocation interfering with template-based block tracking, but in
4441
// `renderSlot` we can be sure that it's template-based so we can force
4542
// enable it.
46-
isRenderingCompiledSlot++
43+
if (slot && (slot as ContextualRenderFn)._c) {
44+
;(slot as ContextualRenderFn)._d = false
45+
}
4746
openBlock()
4847
const validSlotContent = slot && ensureValidVNode(slot(props))
4948
const rendered = createBlock(
@@ -57,7 +56,9 @@ export function renderSlot(
5756
if (!noSlotted && rendered.scopeId) {
5857
rendered.slotScopeIds = [rendered.scopeId + '-s']
5958
}
60-
isRenderingCompiledSlot--
59+
if (slot && (slot as ContextualRenderFn)._c) {
60+
;(slot as ContextualRenderFn)._d = true
61+
}
6162
return rendered
6263
}
6364

packages/runtime-core/src/vnode.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ import {
4040
import { RendererNode, RendererElement } from './renderer'
4141
import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets'
4242
import { hmrDirtyComponents } from './hmr'
43-
import { setCompiledSlotRendering } from './helpers/renderSlot'
4443
import { convertLegacyComponent } from './compat/component'
4544
import { convertLegacyVModelProps } from './compat/componentVModel'
4645
import { defineLegacyVNodeProperties } from './compat/renderFn'
@@ -218,7 +217,7 @@ export function closeBlock() {
218217
// Only tracks when this value is > 0
219218
// We are not using a simple boolean because this value may need to be
220219
// incremented/decremented by nested usage of v-once (see below)
221-
let shouldTrack = 1
220+
let isBlockTreeEnabled = 1
222221

223222
/**
224223
* Block tracking sometimes needs to be disabled, for example during the
@@ -237,7 +236,7 @@ let shouldTrack = 1
237236
* @private
238237
*/
239238
export function setBlockTracking(value: number) {
240-
shouldTrack += value
239+
isBlockTreeEnabled += value
241240
}
242241

243242
/**
@@ -263,12 +262,13 @@ export function createBlock(
263262
true /* isBlock: prevent a block from tracking itself */
264263
)
265264
// save current block children on the block vnode
266-
vnode.dynamicChildren = currentBlock || (EMPTY_ARR as any)
265+
vnode.dynamicChildren =
266+
isBlockTreeEnabled > 0 ? currentBlock || (EMPTY_ARR as any) : null
267267
// close block
268268
closeBlock()
269269
// a block is always going to be patched, so track it as a child of its
270270
// parent block
271-
if (shouldTrack > 0 && currentBlock) {
271+
if (isBlockTreeEnabled > 0 && currentBlock) {
272272
currentBlock.push(vnode)
273273
}
274274
return vnode
@@ -458,7 +458,7 @@ function _createVNode(
458458
}
459459

460460
if (
461-
shouldTrack > 0 &&
461+
isBlockTreeEnabled > 0 &&
462462
// avoid a block node from tracking itself
463463
!isBlockNode &&
464464
// has current parent block
@@ -635,9 +635,9 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
635635
const slot = (children as any).default
636636
if (slot) {
637637
// _c marker is added by withCtx() indicating this is a compiled slot
638-
slot._c && setCompiledSlotRendering(1)
638+
slot._c && (slot._d = false)
639639
normalizeChildren(vnode, slot())
640-
slot._c && setCompiledSlotRendering(-1)
640+
slot._c && (slot._d = true)
641641
}
642642
return
643643
} else {

0 commit comments

Comments
 (0)