diff --git a/packages/runtime-core/__tests__/hmr.spec.ts b/packages/runtime-core/__tests__/hmr.spec.ts
index 991cc0ecdac..9e92ed2a218 100644
--- a/packages/runtime-core/__tests__/hmr.spec.ts
+++ b/packages/runtime-core/__tests__/hmr.spec.ts
@@ -1040,4 +1040,57 @@ describe('hot module replacement', () => {
expect(serializeInner(root)).toBe('
bar
')
})
+
+ // #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}}
+
+ static text`,
+ ),
+ }
+ createRecord(appId, App)
+ render(h(App), root)
+ expect(serializeInner(root)).toBe(`0 static text`)
+
+ // trigger count update
+ triggerEvent((root as any).children[2], 'click')
+ await nextTick()
+ expect(serializeInner(root)).toBe(`1 static text`)
+
+ // trigger HMR update
+ rerender(
+ appId,
+ compileToFunction(
+ `{{count}}
+
+ static text updated`,
+ ),
+ )
+ expect(serializeInner(root)).toBe(
+ `1 static text updated`,
+ )
+
+ // trigger HMR update again
+ rerender(
+ appId,
+ compileToFunction(
+ `{{count}}
+
+ static text updated2`,
+ ),
+ )
+ expect(serializeInner(root)).toBe(
+ `1 static text updated2`,
+ )
+ })
})
diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts
index 192bb44474e..828d1ba0b56 100644
--- a/packages/runtime-core/src/renderer.ts
+++ b/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)