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

close #14127
This commit is contained in:
edison 2025-12-18 16:23:25 +08:00 committed by GitHub
parent 1904053f1f
commit 69ce3c7d75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 84 additions and 6 deletions

View File

@ -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`,
)
})
})

View File

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