]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(hmr): hmr reload should work with async component (#11248)
author_Kerman <kermanx@qq.com>
Mon, 15 Jul 2024 13:54:53 +0000 (21:54 +0800)
committerGitHub <noreply@github.com>
Mon, 15 Jul 2024 13:54:53 +0000 (21:54 +0800)
packages/runtime-core/__tests__/hmr.spec.ts
packages/runtime-core/src/hmr.ts
packages/runtime-core/src/vnode.ts

index 39aece16a5a02364ff220fe8ff0f599f250731ee..ba9c7c0780b3f86811aeb3430e4a259fdf9ba2f1 100644 (file)
@@ -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(`<div>0</div>0`)
+    expect(serializeInner(root)).toBe(`<div>0</div><div>0</div>0`)
 
     reload(childId, {
       __hmrId: childId,
@@ -458,9 +465,9 @@ describe('hot module replacement', () => {
       render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
     })
     await nextTick()
-    expect(serializeInner(root)).toBe(`<div>1</div>1`)
-    expect(unmountSpy).toHaveBeenCalledTimes(1)
-    expect(mountSpy).toHaveBeenCalledTimes(1)
+    expect(serializeInner(root)).toBe(`<div>1</div><div>1</div>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', () => {
       `<div><div>1<p>3</p></div></div><div><div>1<p>3</p></div></div><p>2</p>`,
     )
   })
+
+  // #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(`<div>{{ count }}</div>`),
+    }
+    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(`<div>0</div><div>0</div>`)
+
+    // change count to 1
+    reload(childId, {
+      __hmrId: childId,
+      data() {
+        return { count: 1 }
+      },
+      render: compileToFunction(`<div>{{ count }}</div>`),
+    })
+
+    await timeout()
+
+    expect(serializeInner(root)).toBe(`<div>1</div><div>1</div>`)
+  })
 })
index 8196eb891953d9c4ea6380d47e552d28cbdabdf7..5a4a95705b0ba093244b2fde55d97f84d0862434 100644 (file)
@@ -14,7 +14,10 @@ type HMRComponent = ComponentOptions | ClassComponent
 
 export let isHmrUpdating = false
 
-export const hmrDirtyComponents = new Set<ConcreteComponent>()
+export const hmrDirtyComponents = new Map<
+  ConcreteComponent,
+  Set<ComponentInternalInstance>
+>()
 
 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()
   })
 }
 
index 2210440e717755de171c888cf29b6a0910011145..a0d4074aaea3626aa7102923416fcefc694cfe8f 100644 (file)
@@ -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
 }