}"
`;
-exports[`compiler: codegen > CacheExpression w/ isVNode: true 1`] = `
+exports[`compiler: codegen > CacheExpression w/ isVOnce: true 1`] = `
"
export function render(_ctx, _cache) {
return _cache[1] || (
_setBlockTracking(-1),
- _cache[1] = foo,
+ (_cache[1] = foo).cacheIndex = 1,
_setBlockTracking(1),
_cache[1]
)
expect(code).toMatchSnapshot()
})
- test('CacheExpression w/ isVNode: true', () => {
+ test('CacheExpression w/ isVOnce: true', () => {
const { code } = generate(
createRoot({
cached: 1,
`
_cache[1] || (
_setBlockTracking(-1),
- _cache[1] = foo,
+ (_cache[1] = foo).cacheIndex = 1,
_setBlockTracking(1),
_cache[1]
)
return _cache[0] || (
_setBlockTracking(-1),
- _cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"]),
+ (_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
)
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (
_setBlockTracking(-1),
- _cache[0] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, ["id"]),
+ (_cache[0] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
)
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (
_setBlockTracking(-1),
- _cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"]),
+ (_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
)
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (
_setBlockTracking(-1),
- _cache[0] = _renderSlot($slots, "default"),
+ (_cache[0] = _renderSlot($slots, "default")).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
)
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (
_setBlockTracking(-1),
- _cache[0] = _createElementVNode("div"),
+ (_cache[0] = _createElementVNode("div")).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
)
indent()
push(`${helper(SET_BLOCK_TRACKING)}(-1),`)
newline()
+ push(`(`)
}
push(`_cache[${node.index}] = `)
genNode(node.value, context)
if (node.isVOnce) {
- push(`,`)
+ push(`).cacheIndex = ${node.index},`)
newline()
push(`${helper(SET_BLOCK_TRACKING)}(1),`)
newline()
renderList,
renderSlot,
serialize,
+ setBlockTracking,
withCtx,
} from '@vue/runtime-test'
import { PatchFlags, SlotFlags } from '@vue/shared'
await nextTick()
expect(inner(root)).toBe('<div><!--comment--><div>bar</div></div>')
})
+
+ test('should not take unmount children fast path if children contain cached nodes', async () => {
+ const show = ref(true)
+ const spyUnmounted = vi.fn()
+
+ const Child = {
+ setup() {
+ onUnmounted(spyUnmounted)
+ return () => createVNode('div', null, 'Child')
+ },
+ }
+
+ const app = createApp({
+ render(_: any, cache: any) {
+ return show.value
+ ? (openBlock(),
+ createBlock('div', null, [
+ createVNode('div', null, [
+ cache[0] ||
+ (setBlockTracking(-1),
+ ((cache[0] = createVNode('div', null, [
+ createVNode(Child),
+ ])).cacheIndex = 0),
+ setBlockTracking(1),
+ cache[0]),
+ ]),
+ ]))
+ : createCommentVNode('v-if', true)
+ },
+ })
+
+ app.mount(root)
+ expect(inner(root)).toBe(
+ '<div><div><div><div>Child</div></div></div></div>',
+ )
+
+ show.value = false
+ await nextTick()
+ expect(inner(root)).toBe('<!--v-if-->')
+ expect(spyUnmounted).toHaveBeenCalledTimes(1)
+
+ show.value = true
+ await nextTick()
+ expect(inner(root)).toBe(
+ '<div><div><div><div>Child</div></div></div></div>',
+ )
+
+ // should unmount again, this verifies previous cache was properly cleared
+ show.value = false
+ await nextTick()
+ expect(inner(root)).toBe('<!--v-if-->')
+ expect(spyUnmounted).toHaveBeenCalledTimes(2)
+ })
})
Comment,
Fragment,
Text,
+ type VNode,
cloneVNode,
createBlock,
createVNode,
setBlockTracking(1),
vnode1,
]))
- expect(vnode.dynamicChildren).toStrictEqual([])
+ const expected: VNode['dynamicChildren'] = []
+ expected.hasOnce = true
+ expect(vnode.dynamicChildren).toStrictEqual(expected)
})
// #5657
test('error of slot function execution should not affect block tracking', () => {
)
} else if (
dynamicChildren &&
+ // #5154
+ // when v-once is used inside a block, setBlockTracking(-1) marks the
+ // parent block with hasOnce: true
+ // so that it doesn't take the fast path during unmount - otherwise
+ // components nested in v-once are never unmounted.
+ !dynamicChildren.hasOnce &&
// #1153: fast path should not be taken for non-stable (v-for) fragments
(type !== Fragment ||
(patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT))
/**
* @internal
*/
- dynamicChildren: VNode[] | null
+ dynamicChildren: (VNode[] & { hasOnce?: boolean }) | null
// application root node only
appContext: AppContext | null
// can divide a template into nested blocks, and within each block the node
// structure would be stable. This allows us to skip most children diffing
// and only worry about the dynamic nodes (indicated by patch flags).
-export const blockStack: (VNode[] | null)[] = []
-export let currentBlock: VNode[] | null = null
+export const blockStack: VNode['dynamicChildren'][] = []
+export let currentBlock: VNode['dynamicChildren'] = null
/**
* Open a block.
*/
export function setBlockTracking(value: number) {
isBlockTreeEnabled += value
+ if (value < 0 && currentBlock) {
+ // mark current block so it doesn't take fast path and skip possible
+ // nested components duriung unmount
+ currentBlock.hasOnce = true
+ }
}
function setupBlock(vnode: VNode) {