diff --git a/packages/runtime-core/__tests__/hmr.spec.ts b/packages/runtime-core/__tests__/hmr.spec.ts
index 39aece16a5a..ba9c7c0780b 100644
--- a/packages/runtime-core/__tests__/hmr.spec.ts
+++ b/packages/runtime-core/__tests__/hmr.spec.ts
@@ -29,6 +29,8 @@ function compileToFunction(template: string) {
return render
}
+const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
+
describe('hot module replacement', () => {
test('inject global runtime', () => {
expect(createRecord).toBeDefined()
@@ -436,18 +438,23 @@ describe('hot module replacement', () => {
const Parent: ComponentOptions = {
setup() {
- const com = ref()
- const changeRef = (value: any) => {
- com.value = value
- }
+ const com1 = ref()
+ const changeRef1 = (value: any) => (com1.value = value)
+
+ const com2 = ref()
+ const changeRef2 = (value: any) => (com2.value = value)
- return () => [h(Child, { ref: changeRef }), com.value?.count]
+ return () => [
+ h(Child, { ref: changeRef1 }),
+ h(Child, { ref: changeRef2 }),
+ com1.value?.count,
+ ]
},
}
render(h(Parent), root)
await nextTick()
- expect(serializeInner(root)).toBe(`
0
0`)
+ expect(serializeInner(root)).toBe(`0
0
0`)
reload(childId, {
__hmrId: childId,
@@ -458,9 +465,9 @@ describe('hot module replacement', () => {
render: compileToFunction(`{{ count }}
`),
})
await nextTick()
- expect(serializeInner(root)).toBe(`1
1`)
- expect(unmountSpy).toHaveBeenCalledTimes(1)
- expect(mountSpy).toHaveBeenCalledTimes(1)
+ expect(serializeInner(root)).toBe(`1
1
1`)
+ expect(unmountSpy).toHaveBeenCalledTimes(2)
+ expect(mountSpy).toHaveBeenCalledTimes(2)
})
// #1156 - static nodes should retain DOM element reference across updates
@@ -805,4 +812,43 @@ describe('hot module replacement', () => {
`2
`,
)
})
+
+ // #11248
+ test('reload async component with multiple instances', async () => {
+ const root = nodeOps.createElement('div')
+ const childId = 'test-child-id'
+ const Child: ComponentOptions = {
+ __hmrId: childId,
+ data() {
+ return { count: 0 }
+ },
+ render: compileToFunction(`{{ count }}
`),
+ }
+ const Comp = runtimeTest.defineAsyncComponent(() => Promise.resolve(Child))
+ const appId = 'test-app-id'
+ const App: ComponentOptions = {
+ __hmrId: appId,
+ render: () => [h(Comp), h(Comp)],
+ }
+ createRecord(appId, App)
+
+ render(h(App), root)
+
+ await timeout()
+
+ expect(serializeInner(root)).toBe(`0
0
`)
+
+ // change count to 1
+ reload(childId, {
+ __hmrId: childId,
+ data() {
+ return { count: 1 }
+ },
+ render: compileToFunction(`{{ count }}
`),
+ })
+
+ await timeout()
+
+ expect(serializeInner(root)).toBe(`1
1
`)
+ })
})
diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts
index 8196eb89195..5a4a95705b0 100644
--- a/packages/runtime-core/src/hmr.ts
+++ b/packages/runtime-core/src/hmr.ts
@@ -14,7 +14,10 @@ type HMRComponent = ComponentOptions | ClassComponent
export let isHmrUpdating = false
-export const hmrDirtyComponents = new Set()
+export const hmrDirtyComponents = new Map<
+ ConcreteComponent,
+ Set
+>()
export interface HMRRuntime {
createRecord: typeof createRecord
@@ -110,18 +113,21 @@ function reload(id: string, newComp: HMRComponent) {
// create a snapshot which avoids the set being mutated during updates
const instances = [...record.instances]
- for (const instance of instances) {
+ for (let i = 0; i < instances.length; i++) {
+ const instance = instances[i]
const oldComp = normalizeClassComponent(instance.type as HMRComponent)
- if (!hmrDirtyComponents.has(oldComp)) {
+ let dirtyInstances = hmrDirtyComponents.get(oldComp)
+ if (!dirtyInstances) {
// 1. Update existing comp definition to match new one
if (oldComp !== record.initialDef) {
updateComponentDef(oldComp, newComp)
}
// 2. mark definition dirty. This forces the renderer to replace the
// component on patch.
- hmrDirtyComponents.add(oldComp)
+ hmrDirtyComponents.set(oldComp, (dirtyInstances = new Set()))
}
+ dirtyInstances.add(instance)
// 3. invalidate options resolution cache
instance.appContext.propsCache.delete(instance.type as any)
@@ -131,9 +137,9 @@ function reload(id: string, newComp: HMRComponent) {
// 4. actually update
if (instance.ceReload) {
// custom element
- hmrDirtyComponents.add(oldComp)
+ dirtyInstances.add(instance)
instance.ceReload((newComp as any).styles)
- hmrDirtyComponents.delete(oldComp)
+ dirtyInstances.delete(instance)
} else if (instance.parent) {
// 4. Force the parent instance to re-render. This will cause all updated
// components to be unmounted and re-mounted. Queue the update so that we
@@ -141,8 +147,8 @@ function reload(id: string, newComp: HMRComponent) {
instance.parent.effect.dirty = true
queueJob(() => {
instance.parent!.update()
- // #6930 avoid infinite recursion
- hmrDirtyComponents.delete(oldComp)
+ // #6930, #11248 avoid infinite recursion
+ dirtyInstances.delete(instance)
})
} else if (instance.appContext.reload) {
// root instance mounted via createApp() has a reload method
@@ -159,11 +165,7 @@ function reload(id: string, newComp: HMRComponent) {
// 5. make sure to cleanup dirty hmr components after update
queuePostFlushCb(() => {
- for (const instance of instances) {
- hmrDirtyComponents.delete(
- normalizeClassComponent(instance.type as HMRComponent),
- )
- }
+ hmrDirtyComponents.clear()
})
}
diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts
index 2210440e717..a0d4074aaea 100644
--- a/packages/runtime-core/src/vnode.ts
+++ b/packages/runtime-core/src/vnode.ts
@@ -387,17 +387,16 @@ export function isVNode(value: any): value is VNode {
}
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
- if (
- __DEV__ &&
- n2.shapeFlag & ShapeFlags.COMPONENT &&
- hmrDirtyComponents.has(n2.type as ConcreteComponent)
- ) {
- // #7042, ensure the vnode being unmounted during HMR
- // bitwise operations to remove keep alive flags
- n1.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
- n2.shapeFlag &= ~ShapeFlags.COMPONENT_KEPT_ALIVE
- // HMR only: if the component has been hot-updated, force a reload.
- return false
+ if (__DEV__ && n2.shapeFlag & ShapeFlags.COMPONENT && n1.component) {
+ const dirtyInstances = hmrDirtyComponents.get(n2.type as ConcreteComponent)
+ if (dirtyInstances && dirtyInstances.has(n1.component)) {
+ // #7042, ensure the vnode being unmounted during HMR
+ // bitwise operations to remove keep alive flags
+ n1.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+ n2.shapeFlag &= ~ShapeFlags.COMPONENT_KEPT_ALIVE
+ // HMR only: if the component has been hot-updated, force a reload.
+ return false
+ }
}
return n1.type === n2.type && n1.key === n2.key
}