Skip to content

Commit c8b9794

Browse files
authoredJul 15, 2024··
fix(hmr): hmr reload should work with async component (#11248)
1 parent 1676f07 commit c8b9794

File tree

3 files changed

+80
-33
lines changed

3 files changed

+80
-33
lines changed
 

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

+55-9
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ function compileToFunction(template: string) {
2929
return render
3030
}
3131

32+
const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
33+
3234
describe('hot module replacement', () => {
3335
test('inject global runtime', () => {
3436
expect(createRecord).toBeDefined()
@@ -436,18 +438,23 @@ describe('hot module replacement', () => {
436438

437439
const Parent: ComponentOptions = {
438440
setup() {
439-
const com = ref()
440-
const changeRef = (value: any) => {
441-
com.value = value
442-
}
441+
const com1 = ref()
442+
const changeRef1 = (value: any) => (com1.value = value)
443+
444+
const com2 = ref()
445+
const changeRef2 = (value: any) => (com2.value = value)
443446

444-
return () => [h(Child, { ref: changeRef }), com.value?.count]
447+
return () => [
448+
h(Child, { ref: changeRef1 }),
449+
h(Child, { ref: changeRef2 }),
450+
com1.value?.count,
451+
]
445452
},
446453
}
447454

448455
render(h(Parent), root)
449456
await nextTick()
450-
expect(serializeInner(root)).toBe(`<div>0</div>0`)
457+
expect(serializeInner(root)).toBe(`<div>0</div><div>0</div>0`)
451458

452459
reload(childId, {
453460
__hmrId: childId,
@@ -458,9 +465,9 @@ describe('hot module replacement', () => {
458465
render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
459466
})
460467
await nextTick()
461-
expect(serializeInner(root)).toBe(`<div>1</div>1`)
462-
expect(unmountSpy).toHaveBeenCalledTimes(1)
463-
expect(mountSpy).toHaveBeenCalledTimes(1)
468+
expect(serializeInner(root)).toBe(`<div>1</div><div>1</div>1`)
469+
expect(unmountSpy).toHaveBeenCalledTimes(2)
470+
expect(mountSpy).toHaveBeenCalledTimes(2)
464471
})
465472

466473
// #1156 - static nodes should retain DOM element reference across updates
@@ -805,4 +812,43 @@ describe('hot module replacement', () => {
805812
`<div><div>1<p>3</p></div></div><div><div>1<p>3</p></div></div><p>2</p>`,
806813
)
807814
})
815+
816+
// #11248
817+
test('reload async component with multiple instances', async () => {
818+
const root = nodeOps.createElement('div')
819+
const childId = 'test-child-id'
820+
const Child: ComponentOptions = {
821+
__hmrId: childId,
822+
data() {
823+
return { count: 0 }
824+
},
825+
render: compileToFunction(`<div>{{ count }}</div>`),
826+
}
827+
const Comp = runtimeTest.defineAsyncComponent(() => Promise.resolve(Child))
828+
const appId = 'test-app-id'
829+
const App: ComponentOptions = {
830+
__hmrId: appId,
831+
render: () => [h(Comp), h(Comp)],
832+
}
833+
createRecord(appId, App)
834+
835+
render(h(App), root)
836+
837+
await timeout()
838+
839+
expect(serializeInner(root)).toBe(`<div>0</div><div>0</div>`)
840+
841+
// change count to 1
842+
reload(childId, {
843+
__hmrId: childId,
844+
data() {
845+
return { count: 1 }
846+
},
847+
render: compileToFunction(`<div>{{ count }}</div>`),
848+
})
849+
850+
await timeout()
851+
852+
expect(serializeInner(root)).toBe(`<div>1</div><div>1</div>`)
853+
})
808854
})

‎packages/runtime-core/src/hmr.ts

+15-13
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ type HMRComponent = ComponentOptions | ClassComponent
1414

1515
export let isHmrUpdating = false
1616

17-
export const hmrDirtyComponents = new Set<ConcreteComponent>()
17+
export const hmrDirtyComponents = new Map<
18+
ConcreteComponent,
19+
Set<ComponentInternalInstance>
20+
>()
1821

1922
export interface HMRRuntime {
2023
createRecord: typeof createRecord
@@ -110,18 +113,21 @@ function reload(id: string, newComp: HMRComponent) {
110113
// create a snapshot which avoids the set being mutated during updates
111114
const instances = [...record.instances]
112115

113-
for (const instance of instances) {
116+
for (let i = 0; i < instances.length; i++) {
117+
const instance = instances[i]
114118
const oldComp = normalizeClassComponent(instance.type as HMRComponent)
115119

116-
if (!hmrDirtyComponents.has(oldComp)) {
120+
let dirtyInstances = hmrDirtyComponents.get(oldComp)
121+
if (!dirtyInstances) {
117122
// 1. Update existing comp definition to match new one
118123
if (oldComp !== record.initialDef) {
119124
updateComponentDef(oldComp, newComp)
120125
}
121126
// 2. mark definition dirty. This forces the renderer to replace the
122127
// component on patch.
123-
hmrDirtyComponents.add(oldComp)
128+
hmrDirtyComponents.set(oldComp, (dirtyInstances = new Set()))
124129
}
130+
dirtyInstances.add(instance)
125131

126132
// 3. invalidate options resolution cache
127133
instance.appContext.propsCache.delete(instance.type as any)
@@ -131,18 +137,18 @@ function reload(id: string, newComp: HMRComponent) {
131137
// 4. actually update
132138
if (instance.ceReload) {
133139
// custom element
134-
hmrDirtyComponents.add(oldComp)
140+
dirtyInstances.add(instance)
135141
instance.ceReload((newComp as any).styles)
136-
hmrDirtyComponents.delete(oldComp)
142+
dirtyInstances.delete(instance)
137143
} else if (instance.parent) {
138144
// 4. Force the parent instance to re-render. This will cause all updated
139145
// components to be unmounted and re-mounted. Queue the update so that we
140146
// don't end up forcing the same parent to re-render multiple times.
141147
instance.parent.effect.dirty = true
142148
queueJob(() => {
143149
instance.parent!.update()
144-
// #6930 avoid infinite recursion
145-
hmrDirtyComponents.delete(oldComp)
150+
// #6930, #11248 avoid infinite recursion
151+
dirtyInstances.delete(instance)
146152
})
147153
} else if (instance.appContext.reload) {
148154
// root instance mounted via createApp() has a reload method
@@ -159,11 +165,7 @@ function reload(id: string, newComp: HMRComponent) {
159165

160166
// 5. make sure to cleanup dirty hmr components after update
161167
queuePostFlushCb(() => {
162-
for (const instance of instances) {
163-
hmrDirtyComponents.delete(
164-
normalizeClassComponent(instance.type as HMRComponent),
165-
)
166-
}
168+
hmrDirtyComponents.clear()
167169
})
168170
}
169171

‎packages/runtime-core/src/vnode.ts

+10-11
Original file line numberDiff line numberDiff line change
@@ -387,17 +387,16 @@ export function isVNode(value: any): value is VNode {
387387
}
388388

389389
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
390-
if (
391-
__DEV__ &&
392-
n2.shapeFlag & ShapeFlags.COMPONENT &&
393-
hmrDirtyComponents.has(n2.type as ConcreteComponent)
394-
) {
395-
// #7042, ensure the vnode being unmounted during HMR
396-
// bitwise operations to remove keep alive flags
397-
n1.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
398-
n2.shapeFlag &= ~ShapeFlags.COMPONENT_KEPT_ALIVE
399-
// HMR only: if the component has been hot-updated, force a reload.
400-
return false
390+
if (__DEV__ && n2.shapeFlag & ShapeFlags.COMPONENT && n1.component) {
391+
const dirtyInstances = hmrDirtyComponents.get(n2.type as ConcreteComponent)
392+
if (dirtyInstances && dirtyInstances.has(n1.component)) {
393+
// #7042, ensure the vnode being unmounted during HMR
394+
// bitwise operations to remove keep alive flags
395+
n1.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
396+
n2.shapeFlag &= ~ShapeFlags.COMPONENT_KEPT_ALIVE
397+
// HMR only: if the component has been hot-updated, force a reload.
398+
return false
399+
}
401400
}
402401
return n1.type === n2.type && n1.key === n2.key
403402
}

0 commit comments

Comments
 (0)
Please sign in to comment.