Skip to content

fix(runtime-core): should not track dynamic children when the user calls a compiled slot inside template expression #3554

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
h,
Fragment,
createVNode,
createCommentVNode,
openBlock,
createBlock,
render,
Expand Down Expand Up @@ -576,4 +577,119 @@ describe('renderer: optimized mode', () => {
await nextTick()
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>')
})
})
3 changes: 2 additions & 1 deletion packages/runtime-core/src/compat/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-core/src/compat/renderFn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
}
}
}
Expand Down
46 changes: 33 additions & 13 deletions packages/runtime-core/src/componentRenderContext.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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 `<slot>`.
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 `<slot>`, 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__) {
Expand All @@ -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
}
13 changes: 8 additions & 5 deletions packages/runtime-core/src/componentSlots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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: ` +
Expand All @@ -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,
Expand Down
13 changes: 7 additions & 6 deletions packages/runtime-core/src/helpers/renderSlot.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Data } from '../component'
import { Slots, RawSlots } from '../componentSlots'
import { ContextualRenderFn } from '../componentRenderContext'
import { Comment, isVNode } from '../vnode'
import {
VNodeArrayChildren,
Expand All @@ -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 `<slot/>`
* @private
Expand Down Expand Up @@ -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(
Expand All @@ -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
}

Expand Down
16 changes: 8 additions & 8 deletions packages/runtime-core/src/vnode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -237,7 +236,7 @@ let shouldTrack = 1
* @private
*/
export function setBlockTracking(value: number) {
shouldTrack += value
isBlockTreeEnabled += value
}

/**
Expand All @@ -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
Expand Down Expand Up @@ -458,7 +458,7 @@ function _createVNode(
}

if (
shouldTrack > 0 &&
isBlockTreeEnabled > 0 &&
// avoid a block node from tracking itself
!isBlockNode &&
// has current parent block
Expand Down Expand Up @@ -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 {
Expand Down