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
This commit is contained in:
parent
1526f94edf
commit
201060717d
@ -2,6 +2,7 @@ import {
|
|||||||
h,
|
h,
|
||||||
Fragment,
|
Fragment,
|
||||||
createVNode,
|
createVNode,
|
||||||
|
createCommentVNode,
|
||||||
openBlock,
|
openBlock,
|
||||||
createBlock,
|
createBlock,
|
||||||
render,
|
render,
|
||||||
@ -576,4 +577,119 @@ describe('renderer: optimized mode', () => {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
expect(inner(root)).toBe('<div>World</div>')
|
expect(inner(root)).toBe('<div>World</div>')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// #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('<section><div class="foo"></div></section>')
|
||||||
|
/**
|
||||||
|
* 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('<p>1</p>')
|
||||||
|
|
||||||
|
index.value = 1
|
||||||
|
await nextTick()
|
||||||
|
expect(inner(root)).toBe('<p>2</p>')
|
||||||
|
|
||||||
|
index.value = 0
|
||||||
|
await nextTick()
|
||||||
|
expect(inner(root)).toBe('<p>1</p>')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -37,6 +37,7 @@ import {
|
|||||||
import { resolveFilter } from '../helpers/resolveAssets'
|
import { resolveFilter } from '../helpers/resolveAssets'
|
||||||
import { resolveMergedOptions } from '../componentOptions'
|
import { resolveMergedOptions } from '../componentOptions'
|
||||||
import { InternalSlots, Slots } from '../componentSlots'
|
import { InternalSlots, Slots } from '../componentSlots'
|
||||||
|
import { ContextualRenderFn } from '../componentRenderContext'
|
||||||
|
|
||||||
export type LegacyPublicInstance = ComponentPublicInstance &
|
export type LegacyPublicInstance = ComponentPublicInstance &
|
||||||
LegacyPublicProperties
|
LegacyPublicProperties
|
||||||
@ -106,7 +107,7 @@ export function installCompatInstanceProperties(map: PublicPropertiesMap) {
|
|||||||
const res: InternalSlots = {}
|
const res: InternalSlots = {}
|
||||||
for (const key in i.slots) {
|
for (const key in i.slots) {
|
||||||
const fn = i.slots[key]!
|
const fn = i.slots[key]!
|
||||||
if (!(fn as any)._nonScoped) {
|
if (!(fn as ContextualRenderFn)._ns /* non-scoped slot */) {
|
||||||
res[key] = fn
|
res[key] = fn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -281,7 +281,7 @@ function convertLegacySlots(vnode: VNode): VNode {
|
|||||||
for (const key in slots) {
|
for (const key in slots) {
|
||||||
const slotChildren = slots[key]
|
const slotChildren = slots[key]
|
||||||
slots[key] = () => slotChildren
|
slots[key] = () => slotChildren
|
||||||
slots[key]._nonScoped = true
|
slots[key]._ns = true /* non-scoped slot */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { ComponentInternalInstance } from './component'
|
import { ComponentInternalInstance } from './component'
|
||||||
import { devtoolsComponentUpdated } from './devtools'
|
import { devtoolsComponentUpdated } from './devtools'
|
||||||
import { isRenderingCompiledSlot } from './helpers/renderSlot'
|
import { setBlockTracking } from './vnode'
|
||||||
import { closeBlock, openBlock } from './vnode'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* mark the current rendering instance for asset resolution (e.g.
|
* mark the current rendering instance for asset resolution (e.g.
|
||||||
@ -56,6 +55,14 @@ export function popScopeId() {
|
|||||||
*/
|
*/
|
||||||
export const withScopeId = (_id: string) => withCtx
|
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
|
* Wrap a slot function to memoize current rendering instance
|
||||||
* @private compiler helper
|
* @private compiler helper
|
||||||
@ -66,18 +73,26 @@ export function withCtx(
|
|||||||
isNonScopedSlot?: boolean // __COMPAT__ only
|
isNonScopedSlot?: boolean // __COMPAT__ only
|
||||||
) {
|
) {
|
||||||
if (!ctx) return fn
|
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
|
// 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
|
// can mess up block tracking, so by default we disable block tracking and
|
||||||
// avoid that. This isn't necessary if rendering a compiled `<slot>`.
|
// force bail out when invoking a compiled slot (indicated by the ._d flag).
|
||||||
if (!isRenderingCompiledSlot) {
|
// This isn't necessary if rendering a compiled `<slot>`, so we flip the
|
||||||
openBlock(true /* null block that disables tracking */)
|
// ._d flag off when invoking the wrapped fn inside `renderSlot`.
|
||||||
|
if (renderFnWithContext._d) {
|
||||||
|
setBlockTracking(-1)
|
||||||
}
|
}
|
||||||
const prevInstance = setCurrentRenderingInstance(ctx)
|
const prevInstance = setCurrentRenderingInstance(ctx)
|
||||||
const res = fn(...args)
|
const res = fn(...args)
|
||||||
setCurrentRenderingInstance(prevInstance)
|
setCurrentRenderingInstance(prevInstance)
|
||||||
if (!isRenderingCompiledSlot) {
|
if (renderFnWithContext._d) {
|
||||||
closeBlock()
|
setBlockTracking(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
|
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
|
||||||
@ -86,13 +101,18 @@ export function withCtx(
|
|||||||
|
|
||||||
return res
|
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
|
// this is used in vnode.ts -> normalizeChildren() to set the slot
|
||||||
// rendering flag.
|
// rendering flag.
|
||||||
// also used to cache the normalized results to avoid repeated normalization
|
renderFnWithContext._c = true
|
||||||
renderFnWithContext._c = renderFnWithContext
|
// disable block tracking by default
|
||||||
|
renderFnWithContext._d = true
|
||||||
|
// compat build only flag to distinguish scoped slots from non-scoped ones
|
||||||
if (__COMPAT__ && isNonScopedSlot) {
|
if (__COMPAT__ && isNonScopedSlot) {
|
||||||
renderFnWithContext._nonScoped = true
|
renderFnWithContext._ns = true
|
||||||
}
|
}
|
||||||
return renderFnWithContext
|
return renderFnWithContext
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ import {
|
|||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
import { isKeepAlive } from './components/KeepAlive'
|
import { isKeepAlive } from './components/KeepAlive'
|
||||||
import { withCtx } from './componentRenderContext'
|
import { ContextualRenderFn, withCtx } from './componentRenderContext'
|
||||||
import { isHmrUpdating } from './hmr'
|
import { isHmrUpdating } from './hmr'
|
||||||
import { DeprecationTypes, isCompatEnabled } from './compat/compatConfig'
|
import { DeprecationTypes, isCompatEnabled } from './compat/compatConfig'
|
||||||
import { toRaw } from '@vue/reactivity'
|
import { toRaw } from '@vue/reactivity'
|
||||||
@ -62,9 +62,8 @@ const normalizeSlot = (
|
|||||||
key: string,
|
key: string,
|
||||||
rawSlot: Function,
|
rawSlot: Function,
|
||||||
ctx: ComponentInternalInstance | null | undefined
|
ctx: ComponentInternalInstance | null | undefined
|
||||||
): Slot =>
|
): Slot => {
|
||||||
(rawSlot as any)._c ||
|
const normalized = withCtx((props: any) => {
|
||||||
(withCtx((props: any) => {
|
|
||||||
if (__DEV__ && currentInstance) {
|
if (__DEV__ && currentInstance) {
|
||||||
warn(
|
warn(
|
||||||
`Slot "${key}" invoked outside of the render function: ` +
|
`Slot "${key}" invoked outside of the render function: ` +
|
||||||
@ -73,7 +72,11 @@ const normalizeSlot = (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return normalizeSlotValue(rawSlot(props))
|
return normalizeSlotValue(rawSlot(props))
|
||||||
}, ctx) as Slot)
|
}, ctx) as Slot
|
||||||
|
// NOT a compiled slot
|
||||||
|
;(normalized as ContextualRenderFn)._c = false
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
const normalizeObjectSlots = (
|
const normalizeObjectSlots = (
|
||||||
rawSlots: RawSlots,
|
rawSlots: RawSlots,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Data } from '../component'
|
import { Data } from '../component'
|
||||||
import { Slots, RawSlots } from '../componentSlots'
|
import { Slots, RawSlots } from '../componentSlots'
|
||||||
|
import { ContextualRenderFn } from '../componentRenderContext'
|
||||||
import { Comment, isVNode } from '../vnode'
|
import { Comment, isVNode } from '../vnode'
|
||||||
import {
|
import {
|
||||||
VNodeArrayChildren,
|
VNodeArrayChildren,
|
||||||
@ -11,10 +12,6 @@ import {
|
|||||||
import { PatchFlags, SlotFlags } from '@vue/shared'
|
import { PatchFlags, SlotFlags } from '@vue/shared'
|
||||||
import { warn } from '../warning'
|
import { warn } from '../warning'
|
||||||
|
|
||||||
export let isRenderingCompiledSlot = 0
|
|
||||||
export const setCompiledSlotRendering = (n: number) =>
|
|
||||||
(isRenderingCompiledSlot += n)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compiler runtime helper for rendering `<slot/>`
|
* Compiler runtime helper for rendering `<slot/>`
|
||||||
* @private
|
* @private
|
||||||
@ -43,7 +40,9 @@ export function renderSlot(
|
|||||||
// invocation interfering with template-based block tracking, but in
|
// invocation interfering with template-based block tracking, but in
|
||||||
// `renderSlot` we can be sure that it's template-based so we can force
|
// `renderSlot` we can be sure that it's template-based so we can force
|
||||||
// enable it.
|
// enable it.
|
||||||
isRenderingCompiledSlot++
|
if (slot && (slot as ContextualRenderFn)._c) {
|
||||||
|
;(slot as ContextualRenderFn)._d = false
|
||||||
|
}
|
||||||
openBlock()
|
openBlock()
|
||||||
const validSlotContent = slot && ensureValidVNode(slot(props))
|
const validSlotContent = slot && ensureValidVNode(slot(props))
|
||||||
const rendered = createBlock(
|
const rendered = createBlock(
|
||||||
@ -57,7 +56,9 @@ export function renderSlot(
|
|||||||
if (!noSlotted && rendered.scopeId) {
|
if (!noSlotted && rendered.scopeId) {
|
||||||
rendered.slotScopeIds = [rendered.scopeId + '-s']
|
rendered.slotScopeIds = [rendered.scopeId + '-s']
|
||||||
}
|
}
|
||||||
isRenderingCompiledSlot--
|
if (slot && (slot as ContextualRenderFn)._c) {
|
||||||
|
;(slot as ContextualRenderFn)._d = true
|
||||||
|
}
|
||||||
return rendered
|
return rendered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,6 @@ import {
|
|||||||
import { RendererNode, RendererElement } from './renderer'
|
import { RendererNode, RendererElement } from './renderer'
|
||||||
import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets'
|
import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets'
|
||||||
import { hmrDirtyComponents } from './hmr'
|
import { hmrDirtyComponents } from './hmr'
|
||||||
import { setCompiledSlotRendering } from './helpers/renderSlot'
|
|
||||||
import { convertLegacyComponent } from './compat/component'
|
import { convertLegacyComponent } from './compat/component'
|
||||||
import { convertLegacyVModelProps } from './compat/componentVModel'
|
import { convertLegacyVModelProps } from './compat/componentVModel'
|
||||||
import { defineLegacyVNodeProperties } from './compat/renderFn'
|
import { defineLegacyVNodeProperties } from './compat/renderFn'
|
||||||
@ -218,7 +217,7 @@ export function closeBlock() {
|
|||||||
// Only tracks when this value is > 0
|
// Only tracks when this value is > 0
|
||||||
// We are not using a simple boolean because this value may need to be
|
// We are not using a simple boolean because this value may need to be
|
||||||
// incremented/decremented by nested usage of v-once (see below)
|
// 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
|
* Block tracking sometimes needs to be disabled, for example during the
|
||||||
@ -237,7 +236,7 @@ let shouldTrack = 1
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
export function setBlockTracking(value: number) {
|
export function setBlockTracking(value: number) {
|
||||||
shouldTrack += value
|
isBlockTreeEnabled += value
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -263,12 +262,13 @@ export function createBlock(
|
|||||||
true /* isBlock: prevent a block from tracking itself */
|
true /* isBlock: prevent a block from tracking itself */
|
||||||
)
|
)
|
||||||
// save current block children on the block vnode
|
// 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
|
// close block
|
||||||
closeBlock()
|
closeBlock()
|
||||||
// a block is always going to be patched, so track it as a child of its
|
// a block is always going to be patched, so track it as a child of its
|
||||||
// parent block
|
// parent block
|
||||||
if (shouldTrack > 0 && currentBlock) {
|
if (isBlockTreeEnabled > 0 && currentBlock) {
|
||||||
currentBlock.push(vnode)
|
currentBlock.push(vnode)
|
||||||
}
|
}
|
||||||
return vnode
|
return vnode
|
||||||
@ -458,7 +458,7 @@ function _createVNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
shouldTrack > 0 &&
|
isBlockTreeEnabled > 0 &&
|
||||||
// avoid a block node from tracking itself
|
// avoid a block node from tracking itself
|
||||||
!isBlockNode &&
|
!isBlockNode &&
|
||||||
// has current parent block
|
// has current parent block
|
||||||
@ -635,9 +635,9 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
|
|||||||
const slot = (children as any).default
|
const slot = (children as any).default
|
||||||
if (slot) {
|
if (slot) {
|
||||||
// _c marker is added by withCtx() indicating this is a compiled 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())
|
normalizeChildren(vnode, slot())
|
||||||
slot._c && setCompiledSlotRendering(-1)
|
slot._c && (slot._d = true)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
|
Loading…
Reference in New Issue
Block a user