mirror of https://github.com/vuejs/core.git
fix(v-once): properly unmount v-once cached trees
close #5154 close #8809
This commit is contained in:
parent
3107b57e2e
commit
d343a0dc01
|
@ -19,12 +19,12 @@ export function render(_ctx, _cache) {
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`compiler: codegen > CacheExpression w/ isVNode: true 1`] = `
|
exports[`compiler: codegen > CacheExpression w/ isVOnce: true 1`] = `
|
||||||
"
|
"
|
||||||
export function render(_ctx, _cache) {
|
export function render(_ctx, _cache) {
|
||||||
return _cache[1] || (
|
return _cache[1] || (
|
||||||
_setBlockTracking(-1),
|
_setBlockTracking(-1),
|
||||||
_cache[1] = foo,
|
(_cache[1] = foo).cacheIndex = 1,
|
||||||
_setBlockTracking(1),
|
_setBlockTracking(1),
|
||||||
_cache[1]
|
_cache[1]
|
||||||
)
|
)
|
||||||
|
|
|
@ -437,7 +437,7 @@ describe('compiler: codegen', () => {
|
||||||
expect(code).toMatchSnapshot()
|
expect(code).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('CacheExpression w/ isVNode: true', () => {
|
test('CacheExpression w/ isVOnce: true', () => {
|
||||||
const { code } = generate(
|
const { code } = generate(
|
||||||
createRoot({
|
createRoot({
|
||||||
cached: 1,
|
cached: 1,
|
||||||
|
@ -456,7 +456,7 @@ describe('compiler: codegen', () => {
|
||||||
`
|
`
|
||||||
_cache[1] || (
|
_cache[1] || (
|
||||||
_setBlockTracking(-1),
|
_setBlockTracking(-1),
|
||||||
_cache[1] = foo,
|
(_cache[1] = foo).cacheIndex = 1,
|
||||||
_setBlockTracking(1),
|
_setBlockTracking(1),
|
||||||
_cache[1]
|
_cache[1]
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,7 +9,7 @@ return function render(_ctx, _cache) {
|
||||||
|
|
||||||
return _cache[0] || (
|
return _cache[0] || (
|
||||||
_setBlockTracking(-1),
|
_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),
|
_setBlockTracking(1),
|
||||||
_cache[0]
|
_cache[0]
|
||||||
)
|
)
|
||||||
|
@ -29,7 +29,7 @@ return function render(_ctx, _cache) {
|
||||||
return (_openBlock(), _createElementBlock("div", null, [
|
return (_openBlock(), _createElementBlock("div", null, [
|
||||||
_cache[0] || (
|
_cache[0] || (
|
||||||
_setBlockTracking(-1),
|
_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),
|
_setBlockTracking(1),
|
||||||
_cache[0]
|
_cache[0]
|
||||||
)
|
)
|
||||||
|
@ -48,7 +48,7 @@ return function render(_ctx, _cache) {
|
||||||
return (_openBlock(), _createElementBlock("div", null, [
|
return (_openBlock(), _createElementBlock("div", null, [
|
||||||
_cache[0] || (
|
_cache[0] || (
|
||||||
_setBlockTracking(-1),
|
_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),
|
_setBlockTracking(1),
|
||||||
_cache[0]
|
_cache[0]
|
||||||
)
|
)
|
||||||
|
@ -67,7 +67,7 @@ return function render(_ctx, _cache) {
|
||||||
return (_openBlock(), _createElementBlock("div", null, [
|
return (_openBlock(), _createElementBlock("div", null, [
|
||||||
_cache[0] || (
|
_cache[0] || (
|
||||||
_setBlockTracking(-1),
|
_setBlockTracking(-1),
|
||||||
_cache[0] = _renderSlot($slots, "default"),
|
(_cache[0] = _renderSlot($slots, "default")).cacheIndex = 0,
|
||||||
_setBlockTracking(1),
|
_setBlockTracking(1),
|
||||||
_cache[0]
|
_cache[0]
|
||||||
)
|
)
|
||||||
|
@ -86,7 +86,7 @@ return function render(_ctx, _cache) {
|
||||||
return (_openBlock(), _createElementBlock("div", null, [
|
return (_openBlock(), _createElementBlock("div", null, [
|
||||||
_cache[0] || (
|
_cache[0] || (
|
||||||
_setBlockTracking(-1),
|
_setBlockTracking(-1),
|
||||||
_cache[0] = _createElementVNode("div"),
|
(_cache[0] = _createElementVNode("div")).cacheIndex = 0,
|
||||||
_setBlockTracking(1),
|
_setBlockTracking(1),
|
||||||
_cache[0]
|
_cache[0]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1041,11 +1041,12 @@ function genCacheExpression(node: CacheExpression, context: CodegenContext) {
|
||||||
indent()
|
indent()
|
||||||
push(`${helper(SET_BLOCK_TRACKING)}(-1),`)
|
push(`${helper(SET_BLOCK_TRACKING)}(-1),`)
|
||||||
newline()
|
newline()
|
||||||
|
push(`(`)
|
||||||
}
|
}
|
||||||
push(`_cache[${node.index}] = `)
|
push(`_cache[${node.index}] = `)
|
||||||
genNode(node.value, context)
|
genNode(node.value, context)
|
||||||
if (node.isVOnce) {
|
if (node.isVOnce) {
|
||||||
push(`,`)
|
push(`).cacheIndex = ${node.index},`)
|
||||||
newline()
|
newline()
|
||||||
push(`${helper(SET_BLOCK_TRACKING)}(1),`)
|
push(`${helper(SET_BLOCK_TRACKING)}(1),`)
|
||||||
newline()
|
newline()
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
renderList,
|
renderList,
|
||||||
renderSlot,
|
renderSlot,
|
||||||
serialize,
|
serialize,
|
||||||
|
setBlockTracking,
|
||||||
withCtx,
|
withCtx,
|
||||||
} from '@vue/runtime-test'
|
} from '@vue/runtime-test'
|
||||||
import { PatchFlags, SlotFlags } from '@vue/shared'
|
import { PatchFlags, SlotFlags } from '@vue/shared'
|
||||||
|
@ -1178,4 +1179,57 @@ describe('renderer: optimized mode', () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(inner(root)).toBe('<div><!--comment--><div>bar</div></div>')
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {
|
||||||
Comment,
|
Comment,
|
||||||
Fragment,
|
Fragment,
|
||||||
Text,
|
Text,
|
||||||
|
type VNode,
|
||||||
cloneVNode,
|
cloneVNode,
|
||||||
createBlock,
|
createBlock,
|
||||||
createVNode,
|
createVNode,
|
||||||
|
@ -633,7 +634,9 @@ describe('vnode', () => {
|
||||||
setBlockTracking(1),
|
setBlockTracking(1),
|
||||||
vnode1,
|
vnode1,
|
||||||
]))
|
]))
|
||||||
expect(vnode.dynamicChildren).toStrictEqual([])
|
const expected: VNode['dynamicChildren'] = []
|
||||||
|
expected.hasOnce = true
|
||||||
|
expect(vnode.dynamicChildren).toStrictEqual(expected)
|
||||||
})
|
})
|
||||||
// #5657
|
// #5657
|
||||||
test('error of slot function execution should not affect block tracking', () => {
|
test('error of slot function execution should not affect block tracking', () => {
|
||||||
|
|
|
@ -2164,6 +2164,12 @@ function baseCreateRenderer(
|
||||||
)
|
)
|
||||||
} else if (
|
} else if (
|
||||||
dynamicChildren &&
|
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
|
// #1153: fast path should not be taken for non-stable (v-for) fragments
|
||||||
(type !== Fragment ||
|
(type !== Fragment ||
|
||||||
(patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT))
|
(patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT))
|
||||||
|
|
|
@ -226,7 +226,7 @@ export interface VNode<
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
dynamicChildren: VNode[] | null
|
dynamicChildren: (VNode[] & { hasOnce?: boolean }) | null
|
||||||
|
|
||||||
// application root node only
|
// application root node only
|
||||||
appContext: AppContext | null
|
appContext: AppContext | null
|
||||||
|
@ -259,8 +259,8 @@ export interface VNode<
|
||||||
// can divide a template into nested blocks, and within each block the node
|
// 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
|
// structure would be stable. This allows us to skip most children diffing
|
||||||
// and only worry about the dynamic nodes (indicated by patch flags).
|
// and only worry about the dynamic nodes (indicated by patch flags).
|
||||||
export const blockStack: (VNode[] | null)[] = []
|
export const blockStack: VNode['dynamicChildren'][] = []
|
||||||
export let currentBlock: VNode[] | null = null
|
export let currentBlock: VNode['dynamicChildren'] = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a block.
|
* Open a block.
|
||||||
|
@ -311,6 +311,11 @@ export let isBlockTreeEnabled = 1
|
||||||
*/
|
*/
|
||||||
export function setBlockTracking(value: number) {
|
export function setBlockTracking(value: number) {
|
||||||
isBlockTreeEnabled += value
|
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) {
|
function setupBlock(vnode: VNode) {
|
||||||
|
|
Loading…
Reference in New Issue