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)