]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(hmr): fix hmr for components with no active instance yet
authorEvan You <yyx990803@gmail.com>
Fri, 8 Oct 2021 16:39:24 +0000 (12:39 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 8 Oct 2021 16:39:24 +0000 (12:39 -0400)
fix #4757

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

index 0a5821c8d755a34003afa0b4f039083c4114adae..eaef8d401a75b201da2f6ea0ed768615582f6ea6 100644 (file)
@@ -36,9 +36,9 @@ describe('hot module replacement', () => {
   })
 
   test('createRecord', () => {
-    expect(createRecord('test1')).toBe(true)
+    expect(createRecord('test1', {})).toBe(true)
     // if id has already been created, should return false
-    expect(createRecord('test1')).toBe(false)
+    expect(createRecord('test1', {})).toBe(false)
   })
 
   test('rerender', async () => {
@@ -50,7 +50,7 @@ describe('hot module replacement', () => {
       __hmrId: childId,
       render: compileToFunction(`<div><slot/></div>`)
     }
-    createRecord(childId)
+    createRecord(childId, Child)
 
     const Parent: ComponentOptions = {
       __hmrId: parentId,
@@ -62,7 +62,7 @@ describe('hot module replacement', () => {
         `<div @click="count++">{{ count }}<Child>{{ count }}</Child></div>`
       )
     }
-    createRecord(parentId)
+    createRecord(parentId, Parent)
 
     render(h(Parent), root)
     expect(serializeInner(root)).toBe(`<div>0<div>0</div></div>`)
@@ -128,7 +128,7 @@ describe('hot module replacement', () => {
       unmounted: unmountSpy,
       render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
     }
-    createRecord(childId)
+    createRecord(childId, Child)
 
     const Parent: ComponentOptions = {
       render: () => h(Child)
@@ -167,7 +167,7 @@ describe('hot module replacement', () => {
         render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
       }
     }
-    createRecord(childId)
+    createRecord(childId, Child)
 
     const Parent: ComponentOptions = {
       render: () => h(Child)
@@ -212,7 +212,7 @@ describe('hot module replacement', () => {
       },
       render: compileToFunction(template)
     }
-    createRecord(id)
+    createRecord(id, Comp)
 
     render(h(Comp), root)
     expect(serializeInner(root)).toBe(
@@ -249,14 +249,14 @@ describe('hot module replacement', () => {
       },
       render: compileToFunction(`<div>{{ msg }}</div>`)
     }
-    createRecord(childId)
+    createRecord(childId, Child)
 
     const Parent: ComponentOptions = {
       __hmrId: parentId,
       components: { Child },
       render: compileToFunction(`<Child msg="foo" />`)
     }
-    createRecord(parentId)
+    createRecord(parentId, Parent)
 
     render(h(Parent), root)
     expect(serializeInner(root)).toBe(`<div>foo</div>`)
@@ -275,14 +275,14 @@ describe('hot module replacement', () => {
       __hmrId: childId,
       render: compileToFunction(`<div>child</div>`)
     }
-    createRecord(childId)
+    createRecord(childId, Child)
 
     const Parent: ComponentOptions = {
       __hmrId: parentId,
       components: { Child },
       render: compileToFunction(`<Child class="test" />`)
     }
-    createRecord(parentId)
+    createRecord(parentId, Parent)
 
     render(h(Parent), root)
     expect(serializeInner(root)).toBe(`<div class="test">child</div>`)
@@ -302,7 +302,7 @@ describe('hot module replacement', () => {
       __hmrId: childId,
       render: compileToFunction(`<div>child</div>`)
     }
-    createRecord(childId)
+    createRecord(childId, Child)
 
     const components: ComponentOptions[] = []
 
@@ -324,7 +324,7 @@ describe('hot module replacement', () => {
         }
       }
 
-      createRecord(parentId)
+      createRecord(parentId, parentComp)
     }
 
     const last = components[components.length - 1]
@@ -370,7 +370,7 @@ describe('hot module replacement', () => {
         </Child>
       `)
     }
-    createRecord(parentId)
+    createRecord(parentId, Parent)
 
     render(h(Parent), root)
     expect(serializeInner(root)).toBe(
@@ -410,7 +410,7 @@ describe('hot module replacement', () => {
         return h('div')
       }
     }
-    createRecord(childId)
+    createRecord(childId, Child)
 
     const Parent: ComponentOptions = {
       render: () => h(Child)
@@ -435,4 +435,38 @@ describe('hot module replacement', () => {
     expect(createSpy1).toHaveBeenCalledTimes(1)
     expect(createSpy2).toHaveBeenCalledTimes(1)
   })
+
+  // #4757
+  test('rerender for component that has no active instance yet', () => {
+    const id = 'no-active-instance-rerender'
+    const Foo: ComponentOptions = {
+      __hmrId: id,
+      render: () => 'foo'
+    }
+
+    createRecord(id, Foo)
+    rerender(id, () => 'bar')
+
+    const root = nodeOps.createElement('div')
+    render(h(Foo), root)
+    expect(serializeInner(root)).toBe('bar')
+  })
+
+  test('reload for component that has no active instance yet', () => {
+    const id = 'no-active-instance-reload'
+    const Foo: ComponentOptions = {
+      __hmrId: id,
+      render: () => 'foo'
+    }
+
+    createRecord(id, Foo)
+    reload(id, {
+      __hmrId: id,
+      render: () => 'bar'
+    })
+
+    const root = nodeOps.createElement('div')
+    render(h(Foo), root)
+    expect(serializeInner(root)).toBe('bar')
+  })
 })
index 3bd5ef88bc3d45a11ff3386fee6ebd6da5e49d9c..3c3f5208bcc617ebbf24e19ae1e5f08416d140a9 100644 (file)
@@ -10,6 +10,8 @@ import {
 import { queueJob, queuePostFlushCb } from './scheduler'
 import { extend, getGlobalThis } from '@vue/shared'
 
+type HMRComponent = ComponentOptions | ClassComponent
+
 export let isHmrUpdating = false
 
 export const hmrDirtyComponents = new Set<ConcreteComponent>()
@@ -33,32 +35,42 @@ if (__DEV__) {
   } as HMRRuntime
 }
 
-const map: Map<string, Set<ComponentInternalInstance>> = new Map()
+const map: Map<
+  string,
+  {
+    // the initial component definition is recorded on import - this allows us
+    // to apply hot updates to the component even when there are no actively
+    // rendered instance.
+    initialDef: ComponentOptions
+    instances: Set<ComponentInternalInstance>
+  }
+> = new Map()
 
 export function registerHMR(instance: ComponentInternalInstance) {
   const id = instance.type.__hmrId!
   let record = map.get(id)
   if (!record) {
-    createRecord(id)
+    createRecord(id, instance.type as HMRComponent)
     record = map.get(id)!
   }
-  record.add(instance)
+  record.instances.add(instance)
 }
 
 export function unregisterHMR(instance: ComponentInternalInstance) {
-  map.get(instance.type.__hmrId!)!.delete(instance)
+  map.get(instance.type.__hmrId!)!.instances.delete(instance)
 }
 
-function createRecord(id: string): boolean {
+function createRecord(id: string, initialDef: HMRComponent): boolean {
   if (map.has(id)) {
     return false
   }
-  map.set(id, new Set())
+  map.set(id, {
+    initialDef: normalizeClassComponent(initialDef),
+    instances: new Set()
+  })
   return true
 }
 
-type HMRComponent = ComponentOptions | ClassComponent
-
 function normalizeClassComponent(component: HMRComponent): ComponentOptions {
   return isClassComponent(component) ? component.__vccOpts : component
 }
@@ -68,8 +80,12 @@ function rerender(id: string, newRender?: Function) {
   if (!record) {
     return
   }
+
+  // update initial record (for not-yet-rendered component)
+  record.initialDef.render = newRender
+
   // Create a snapshot which avoids the set being mutated during updates
-  ;[...record].forEach(instance => {
+  ;[...record.instances].forEach(instance => {
     if (newRender) {
       instance.render = newRender as InternalRenderFunction
       normalizeClassComponent(instance.type as HMRComponent).render = newRender
@@ -87,20 +103,19 @@ function reload(id: string, newComp: HMRComponent) {
   if (!record) return
 
   newComp = normalizeClassComponent(newComp)
+  // update initial def (for not-yet-rendered components)
+  updateComponentDef(record.initialDef, newComp)
 
   // create a snapshot which avoids the set being mutated during updates
-  const instances = [...record]
+  const instances = [...record.instances]
 
   for (const instance of instances) {
     const oldComp = normalizeClassComponent(instance.type as HMRComponent)
 
     if (!hmrDirtyComponents.has(oldComp)) {
       // 1. Update existing comp definition to match new one
-      extend(oldComp, newComp)
-      for (const key in oldComp) {
-        if (key !== '__file' && !(key in newComp)) {
-          delete (oldComp as any)[key]
-        }
+      if (oldComp !== record.initialDef) {
+        updateComponentDef(oldComp, newComp)
       }
       // 2. mark definition dirty. This forces the renderer to replace the
       // component on patch.
@@ -152,6 +167,18 @@ function reload(id: string, newComp: HMRComponent) {
   })
 }
 
+function updateComponentDef(
+  oldComp: ComponentOptions,
+  newComp: ComponentOptions
+) {
+  extend(oldComp, newComp)
+  for (const key in oldComp) {
+    if (key !== '__file' && !(key in newComp)) {
+      delete (oldComp as any)[key]
+    }
+  }
+}
+
 function tryWrap(fn: (id: string, arg: any) => any): Function {
   return (id: string, arg: any) => {
     try {