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:
HcySunYang 2021-05-26 01:33:41 +08:00 committed by GitHub
parent 1526f94edf
commit 201060717d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 175 additions and 34 deletions

View File

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

View File

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

View File

@ -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 */
} }
} }
} }

View File

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

View File

@ -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,

View File

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

View File

@ -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 {