From 9e3d7731c7839638f49157123c6b372fec9e4d46 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 8 Oct 2021 12:39:24 -0400 Subject: [PATCH] fix(hmr): fix hmr for components with no active instance yet fix #4757 --- packages/runtime-core/__tests__/hmr.spec.ts | 64 ++++++++++++++++----- packages/runtime-core/src/hmr.ts | 57 +++++++++++++----- 2 files changed, 91 insertions(+), 30 deletions(-) diff --git a/packages/runtime-core/__tests__/hmr.spec.ts b/packages/runtime-core/__tests__/hmr.spec.ts index 0a5821c8d75..eaef8d401a7 100644 --- a/packages/runtime-core/__tests__/hmr.spec.ts +++ b/packages/runtime-core/__tests__/hmr.spec.ts @@ -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(`
`) } - createRecord(childId) + createRecord(childId, Child) const Parent: ComponentOptions = { __hmrId: parentId, @@ -62,7 +62,7 @@ describe('hot module replacement', () => { `
{{ count }}{{ count }}
` ) } - createRecord(parentId) + createRecord(parentId, Parent) render(h(Parent), root) expect(serializeInner(root)).toBe(`
0
0
`) @@ -128,7 +128,7 @@ describe('hot module replacement', () => { unmounted: unmountSpy, render: compileToFunction(`
{{ count }}
`) } - createRecord(childId) + createRecord(childId, Child) const Parent: ComponentOptions = { render: () => h(Child) @@ -167,7 +167,7 @@ describe('hot module replacement', () => { render: compileToFunction(`
{{ count }}
`) } } - 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(`
{{ msg }}
`) } - createRecord(childId) + createRecord(childId, Child) const Parent: ComponentOptions = { __hmrId: parentId, components: { Child }, render: compileToFunction(``) } - createRecord(parentId) + createRecord(parentId, Parent) render(h(Parent), root) expect(serializeInner(root)).toBe(`
foo
`) @@ -275,14 +275,14 @@ describe('hot module replacement', () => { __hmrId: childId, render: compileToFunction(`
child
`) } - createRecord(childId) + createRecord(childId, Child) const Parent: ComponentOptions = { __hmrId: parentId, components: { Child }, render: compileToFunction(``) } - createRecord(parentId) + createRecord(parentId, Parent) render(h(Parent), root) expect(serializeInner(root)).toBe(`
child
`) @@ -302,7 +302,7 @@ describe('hot module replacement', () => { __hmrId: childId, render: compileToFunction(`
child
`) } - 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', () => {
`) } - 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') + }) }) diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts index 3bd5ef88bc3..3c3f5208bcc 100644 --- a/packages/runtime-core/src/hmr.ts +++ b/packages/runtime-core/src/hmr.ts @@ -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() @@ -33,32 +35,42 @@ if (__DEV__) { } as HMRRuntime } -const map: Map> = 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 + } +> = 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 {