Ver Fonte

fix(hmr): avoid infinite recursion when reloading hmr components (#6936)

close #6930
Zeke Zhang há 1 ano atrás
pai
commit
36bd9b0a1f

+ 48 - 0
packages/runtime-core/__tests__/hmr.spec.ts

@@ -6,6 +6,7 @@ import {
   h,
   nextTick,
   nodeOps,
+  ref,
   render,
   serializeInner,
   triggerEvent,
@@ -415,6 +416,53 @@ describe('hot module replacement', () => {
     expect(mountSpy).toHaveBeenCalledTimes(1)
   })
 
+  // #6930
+  test('reload: avoid infinite recursion', async () => {
+    const root = nodeOps.createElement('div')
+    const childId = 'test-child-6930'
+    const unmountSpy = vi.fn()
+    const mountSpy = vi.fn()
+
+    const Child: ComponentOptions = {
+      __hmrId: childId,
+      data() {
+        return { count: 0 }
+      },
+      expose: ['count'],
+      unmounted: unmountSpy,
+      render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
+    }
+    createRecord(childId, Child)
+
+    const Parent: ComponentOptions = {
+      setup() {
+        const com = ref()
+        const changeRef = (value: any) => {
+          com.value = value
+        }
+
+        return () => [h(Child, { ref: changeRef }), com.value?.count]
+      },
+    }
+
+    render(h(Parent), root)
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<div>0</div>0`)
+
+    reload(childId, {
+      __hmrId: childId,
+      data() {
+        return { count: 1 }
+      },
+      mounted: mountSpy,
+      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)
+  })
+
   // #1156 - static nodes should retain DOM element reference across updates
   // when HMR is active
   test('static el reference', async () => {

+ 5 - 1
packages/runtime-core/src/hmr.ts

@@ -139,7 +139,11 @@ function reload(id: string, newComp: HMRComponent) {
       // components to be unmounted and re-mounted. Queue the update so that we
       // don't end up forcing the same parent to re-render multiple times.
       instance.parent.effect.dirty = true
-      queueJob(instance.parent.update)
+      queueJob(() => {
+        instance.parent!.update()
+        // #6930 avoid infinite recursion
+        hmrDirtyComponents.delete(oldComp)
+      })
     } else if (instance.appContext.reload) {
       // root instance mounted via createApp() has a reload method
       instance.appContext.reload()