Skip to content

Commit 69ce3c7

Browse files
authored
fix(hmr): handle cached text node update (#14134)
close #14127
1 parent 1904053 commit 69ce3c7

File tree

2 files changed

+84
-6
lines changed

2 files changed

+84
-6
lines changed

packages/runtime-core/__tests__/hmr.spec.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,4 +1040,57 @@ describe('hot module replacement', () => {
10401040

10411041
expect(serializeInner(root)).toBe('<div>bar</div>')
10421042
})
1043+
1044+
// #14127
1045+
test('update cached text nodes', async () => {
1046+
const root = nodeOps.createElement('div')
1047+
const appId = 'test-cached-text-nodes'
1048+
const App: ComponentOptions = {
1049+
__hmrId: appId,
1050+
data() {
1051+
return {
1052+
count: 0,
1053+
}
1054+
},
1055+
render: compileToFunction(
1056+
`{{count}}
1057+
<button @click="count++">++</button>
1058+
static text`,
1059+
),
1060+
}
1061+
createRecord(appId, App)
1062+
render(h(App), root)
1063+
expect(serializeInner(root)).toBe(`0 <button>++</button> static text`)
1064+
1065+
// trigger count update
1066+
triggerEvent((root as any).children[2], 'click')
1067+
await nextTick()
1068+
expect(serializeInner(root)).toBe(`1 <button>++</button> static text`)
1069+
1070+
// trigger HMR update
1071+
rerender(
1072+
appId,
1073+
compileToFunction(
1074+
`{{count}}
1075+
<button @click="count++">++</button>
1076+
static text updated`,
1077+
),
1078+
)
1079+
expect(serializeInner(root)).toBe(
1080+
`1 <button>++</button> static text updated`,
1081+
)
1082+
1083+
// trigger HMR update again
1084+
rerender(
1085+
appId,
1086+
compileToFunction(
1087+
`{{count}}
1088+
<button @click="count++">++</button>
1089+
static text updated2`,
1090+
),
1091+
)
1092+
expect(serializeInner(root)).toBe(
1093+
`1 <button>++</button> static text updated2`,
1094+
)
1095+
})
10431096
})

packages/runtime-core/src/renderer.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,27 @@ function baseCreateRenderer(
500500
} else {
501501
const el = (n2.el = n1.el!)
502502
if (n2.children !== n1.children) {
503-
hostSetText(el, n2.children as string)
503+
// We don't inherit el for cached text nodes in `traverseStaticChildren`
504+
// to avoid retaining detached DOM nodes. However, the text node may be
505+
// changed during HMR. In this case we need to replace the old text node
506+
// with the new one.
507+
if (
508+
__DEV__ &&
509+
isHmrUpdating &&
510+
n2.patchFlag === PatchFlags.CACHED &&
511+
'__elIndex' in n1
512+
) {
513+
const childNodes = __TEST__
514+
? container.children
515+
: container.childNodes
516+
const newChild = hostCreateText(n2.children as string)
517+
const oldChild =
518+
childNodes[((n2 as any).__elIndex = (n1 as any).__elIndex)]
519+
hostInsert(newChild, container, oldChild)
520+
hostRemove(oldChild)
521+
} else {
522+
hostSetText(el, n2.children as string)
523+
}
504524
}
505525
}
506526
}
@@ -2496,12 +2516,17 @@ export function traverseStaticChildren(
24962516
traverseStaticChildren(c1, c2)
24972517
}
24982518
// #6852 also inherit for text nodes
2499-
if (
2500-
c2.type === Text &&
2519+
if (c2.type === Text) {
25012520
// avoid cached text nodes retaining detached dom nodes
2502-
c2.patchFlag !== PatchFlags.CACHED
2503-
) {
2504-
c2.el = c1.el
2521+
if (c2.patchFlag !== PatchFlags.CACHED) {
2522+
c2.el = c1.el
2523+
} else {
2524+
// cache the child index for HMR updates
2525+
;(c2 as any).__elIndex =
2526+
i +
2527+
// take fragment start anchor into account
2528+
(n1.type === Fragment ? 1 : 0)
2529+
}
25052530
}
25062531
// #2324 also inherit for comment nodes, but not placeholders (e.g. v-if which
25072532
// would have received .el during block patch)

0 commit comments

Comments
 (0)