h,
Fragment,
createVNode,
+ createCommentVNode,
openBlock,
createBlock,
render,
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>')
+ })
})
import { resolveFilter } from '../helpers/resolveAssets'
import { resolveMergedOptions } from '../componentOptions'
import { InternalSlots, Slots } from '../componentSlots'
+import { ContextualRenderFn } from '../componentRenderContext'
export type LegacyPublicInstance = ComponentPublicInstance &
LegacyPublicProperties
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
}
}
for (const key in slots) {
const slotChildren = slots[key]
slots[key] = () => slotChildren
- slots[key]._nonScoped = true
+ slots[key]._ns = true /* non-scoped slot */
}
}
}
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.
*/
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
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__) {
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
}
} 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'
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: ` +
)
}
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,
import { Data } from '../component'
import { Slots, RawSlots } from '../componentSlots'
+import { ContextualRenderFn } from '../componentRenderContext'
import { Comment, isVNode } from '../vnode'
import {
VNodeArrayChildren,
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
// 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(
if (!noSlotted && rendered.scopeId) {
rendered.slotScopeIds = [rendered.scopeId + '-s']
}
- isRenderingCompiledSlot--
+ if (slot && (slot as ContextualRenderFn)._c) {
+ ;(slot as ContextualRenderFn)._d = true
+ }
return rendered
}
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'
// 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
* @private
*/
export function setBlockTracking(value: number) {
- shouldTrack += value
+ isBlockTreeEnabled += value
}
/**
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
}
if (
- shouldTrack > 0 &&
+ isBlockTreeEnabled > 0 &&
// avoid a block node from tracking itself
!isBlockNode &&
// has current parent block
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 {