Просмотр исходного кода

fix(hmr): handle cached text node update (#14134)

close #14127
edison 6 месяцев назад
Родитель
Сommit
69ce3c7d75
2 измененных файлов с 84 добавлено и 6 удалено
  1. 53 0
      packages/runtime-core/__tests__/hmr.spec.ts
  2. 31 6
      packages/runtime-core/src/renderer.ts

+ 53 - 0
packages/runtime-core/__tests__/hmr.spec.ts

@@ -1040,4 +1040,57 @@ describe('hot module replacement', () => {
 
     expect(serializeInner(root)).toBe('<div>bar</div>')
   })
+
+  // #14127
+  test('update cached text nodes', async () => {
+    const root = nodeOps.createElement('div')
+    const appId = 'test-cached-text-nodes'
+    const App: ComponentOptions = {
+      __hmrId: appId,
+      data() {
+        return {
+          count: 0,
+        }
+      },
+      render: compileToFunction(
+        `{{count}}
+        <button @click="count++">++</button> 
+        static text`,
+      ),
+    }
+    createRecord(appId, App)
+    render(h(App), root)
+    expect(serializeInner(root)).toBe(`0 <button>++</button> static text`)
+
+    // trigger count update
+    triggerEvent((root as any).children[2], 'click')
+    await nextTick()
+    expect(serializeInner(root)).toBe(`1 <button>++</button> static text`)
+
+    // trigger HMR update
+    rerender(
+      appId,
+      compileToFunction(
+        `{{count}}
+        <button @click="count++">++</button> 
+        static text updated`,
+      ),
+    )
+    expect(serializeInner(root)).toBe(
+      `1 <button>++</button> static text updated`,
+    )
+
+    // trigger HMR update again
+    rerender(
+      appId,
+      compileToFunction(
+        `{{count}}
+        <button @click="count++">++</button> 
+        static text updated2`,
+      ),
+    )
+    expect(serializeInner(root)).toBe(
+      `1 <button>++</button> static text updated2`,
+    )
+  })
 })

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

@@ -500,7 +500,27 @@ function baseCreateRenderer(
     } else {
       const el = (n2.el = n1.el!)
       if (n2.children !== n1.children) {
-        hostSetText(el, n2.children as string)
+        // We don't inherit el for cached text nodes in `traverseStaticChildren`
+        // to avoid retaining detached DOM nodes. However, the text node may be
+        // changed during HMR. In this case we need to replace the old text node
+        // with the new one.
+        if (
+          __DEV__ &&
+          isHmrUpdating &&
+          n2.patchFlag === PatchFlags.CACHED &&
+          '__elIndex' in n1
+        ) {
+          const childNodes = __TEST__
+            ? container.children
+            : container.childNodes
+          const newChild = hostCreateText(n2.children as string)
+          const oldChild =
+            childNodes[((n2 as any).__elIndex = (n1 as any).__elIndex)]
+          hostInsert(newChild, container, oldChild)
+          hostRemove(oldChild)
+        } else {
+          hostSetText(el, n2.children as string)
+        }
       }
     }
   }
@@ -2496,12 +2516,17 @@ export function traverseStaticChildren(
           traverseStaticChildren(c1, c2)
       }
       // #6852 also inherit for text nodes
-      if (
-        c2.type === Text &&
+      if (c2.type === Text) {
         // avoid cached text nodes retaining detached dom nodes
-        c2.patchFlag !== PatchFlags.CACHED
-      ) {
-        c2.el = c1.el
+        if (c2.patchFlag !== PatchFlags.CACHED) {
+          c2.el = c1.el
+        } else {
+          // cache the child index for HMR updates
+          ;(c2 as any).__elIndex =
+            i +
+            // take fragment start anchor into account
+            (n1.type === Fragment ? 1 : 0)
+        }
       }
       // #2324 also inherit for comment nodes, but not placeholders (e.g. v-if which
       // would have received .el during block patch)