2
0
Эх сурвалжийг харах

fix(v-once): properly unmount v-once cached trees

close #5154
close #8809
Evan You 1 жил өмнө
parent
commit
d343a0dc01

+ 2 - 2
packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap

@@ -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]
   )
   )

+ 2 - 2
packages/compiler-core/__tests__/codegen.spec.ts

@@ -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]
   )
   )

+ 5 - 5
packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap

@@ -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]
       )
       )

+ 2 - 1
packages/compiler-core/src/codegen.ts

@@ -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()

+ 54 - 0
packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts

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

+ 4 - 1
packages/runtime-core/__tests__/vnode.spec.ts

@@ -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', () => {

+ 6 - 0
packages/runtime-core/src/renderer.ts

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

+ 8 - 3
packages/runtime-core/src/vnode.ts

@@ -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 let currentBlock: VNode[] | null = null
+export const blockStack: VNode['dynamicChildren'][] = []
+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) {