From c70913bdfe34109b5810981b3e5d8991ad5022ba Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 24 Nov 2025 21:38:18 +0800 Subject: [PATCH 1/3] fix(hmr): handle text node creation during HMR updates for cached nodes --- packages/runtime-core/src/renderer.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 192bb44474e..bfbae18d8a3 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -498,8 +498,14 @@ function baseCreateRenderer( anchor, ) } else { - const el = (n2.el = n1.el!) + let el = (n2.el = n1.el!) if (n2.children !== n1.children) { + // we don't inherit text node for cached text nodes in `traverseStaticChildren` + // but it maybe changed during HMR updates, so we need to handle this case by + // creating a new text node. + if (__DEV__ && isHmrUpdating && n2.patchFlag === PatchFlags.CACHED) { + el = hostCreateText(n2.children as string) + } hostSetText(el, n2.children as string) } } From 7e084180428673820ebb04c81110aacd96e8ec1d Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 25 Nov 2025 08:48:32 +0800 Subject: [PATCH 2/3] fix: improve HMR updates for cached text nodes by replacing them --- packages/runtime-core/src/renderer.ts | 34 +++++++++++++++++++-------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index bfbae18d8a3..a5afba26d6b 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -498,15 +498,27 @@ function baseCreateRenderer( anchor, ) } else { - let el = (n2.el = n1.el!) + const el = (n2.el = n1.el!) if (n2.children !== n1.children) { // we don't inherit text node for cached text nodes in `traverseStaticChildren` // but it maybe changed during HMR updates, so we need to handle this case by - // creating a new text node. - if (__DEV__ && isHmrUpdating && n2.patchFlag === PatchFlags.CACHED) { - el = hostCreateText(n2.children as string) + // replacing the text node. + if ( + __DEV__ && + isHmrUpdating && + n2.patchFlag === PatchFlags.CACHED && + '__elIndex' in n1 + ) { + const newChild = hostCreateText(n2.children as string) + const oldChild = + container.childNodes[ + ((n2 as any).__elIndex = (n1 as any).__elIndex) + ] + hostInsert(newChild, container, oldChild) + hostRemove(oldChild) + } else { + hostSetText(el, n2.children as string) } - hostSetText(el, n2.children as string) } } } @@ -2502,12 +2514,14 @@ 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 + (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) From 6c11c4dc5ef1bdfc58404adf9174915beba176d9 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 25 Nov 2025 09:19:00 +0800 Subject: [PATCH 3/3] test: add test --- packages/runtime-core/__tests__/hmr.spec.ts | 53 +++++++++++++++++++++ packages/runtime-core/src/renderer.ts | 19 +++++--- 2 files changed, 65 insertions(+), 7 deletions(-) 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 a5afba26d6b..828d1ba0b56 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -500,20 +500,22 @@ function baseCreateRenderer( } else { const el = (n2.el = n1.el!) if (n2.children !== n1.children) { - // we don't inherit text node for cached text nodes in `traverseStaticChildren` - // but it maybe changed during HMR updates, so we need to handle this case by - // replacing the text node. + // 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 = - container.childNodes[ - ((n2 as any).__elIndex = (n1 as any).__elIndex) - ] + childNodes[((n2 as any).__elIndex = (n1 as any).__elIndex)] hostInsert(newChild, container, oldChild) hostRemove(oldChild) } else { @@ -2520,7 +2522,10 @@ export function traverseStaticChildren( c2.el = c1.el } else { // cache the child index for HMR updates - ;(c2 as any).__elIndex = i + (n1.type === Fragment ? 1 : 0) + ;(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