diff --git a/packages/runtime-core/__tests__/hmr.spec.ts b/packages/runtime-core/__tests__/hmr.spec.ts index a5d0d67d51d..0a5821c8d75 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, Child) + createRecord(childId) const Parent: ComponentOptions = { __hmrId: parentId, @@ -62,7 +62,7 @@ describe('hot module replacement', () => { `
{{ count }}{{ count }}
` ) } - createRecord(parentId, Parent) + createRecord(parentId) 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, Child) + createRecord(childId) const Parent: ComponentOptions = { render: () => h(Child) @@ -167,7 +167,7 @@ describe('hot module replacement', () => { render: compileToFunction(`
{{ count }}
`) } } - createRecord(childId, Child) + createRecord(childId) const Parent: ComponentOptions = { render: () => h(Child) @@ -212,7 +212,7 @@ describe('hot module replacement', () => { }, render: compileToFunction(template) } - createRecord(id, Comp) + createRecord(id) render(h(Comp), root) expect(serializeInner(root)).toBe( @@ -249,14 +249,14 @@ describe('hot module replacement', () => { }, render: compileToFunction(`
{{ msg }}
`) } - createRecord(childId, Child) + createRecord(childId) const Parent: ComponentOptions = { __hmrId: parentId, components: { Child }, render: compileToFunction(``) } - createRecord(parentId, Parent) + createRecord(parentId) render(h(Parent), root) expect(serializeInner(root)).toBe(`
foo
`) @@ -275,14 +275,14 @@ describe('hot module replacement', () => { __hmrId: childId, render: compileToFunction(`
child
`) } - createRecord(childId, Child) + createRecord(childId) const Parent: ComponentOptions = { __hmrId: parentId, components: { Child }, render: compileToFunction(``) } - createRecord(parentId, Parent) + createRecord(parentId) render(h(Parent), root) expect(serializeInner(root)).toBe(`
child
`) @@ -302,7 +302,7 @@ describe('hot module replacement', () => { __hmrId: childId, render: compileToFunction(`
child
`) } - createRecord(childId, Child) + createRecord(childId) const components: ComponentOptions[] = [] @@ -324,7 +324,7 @@ describe('hot module replacement', () => { } } - createRecord(parentId, parentComp) + createRecord(parentId) } const last = components[components.length - 1] @@ -370,7 +370,7 @@ describe('hot module replacement', () => {
`) } - createRecord(parentId, Parent) + createRecord(parentId) render(h(Parent), root) expect(serializeInner(root)).toBe( @@ -410,7 +410,7 @@ describe('hot module replacement', () => { return h('div') } } - createRecord(childId, Child) + createRecord(childId) const Parent: ComponentOptions = { render: () => h(Child) diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts index c2fbee65afe..358fa843b9b 100644 --- a/packages/runtime-core/src/hmr.ts +++ b/packages/runtime-core/src/hmr.ts @@ -9,7 +9,6 @@ import { } from './component' import { queueJob, queuePostFlushCb } from './scheduler' import { extend } from '@vue/shared' -import { warn } from './warning' export let isHmrUpdating = false @@ -43,58 +42,46 @@ if (__DEV__) { } as HMRRuntime } -type HMRRecord = { - component: ComponentOptions - instances: Set -} - -const map: Map = new Map() +const map: Map> = new Map() export function registerHMR(instance: ComponentInternalInstance) { const id = instance.type.__hmrId! let record = map.get(id) if (!record) { - createRecord(id, instance.type as ComponentOptions) + createRecord(id) record = map.get(id)! } - record.instances.add(instance) + record.add(instance) } export function unregisterHMR(instance: ComponentInternalInstance) { - map.get(instance.type.__hmrId!)!.instances.delete(instance) + map.get(instance.type.__hmrId!)!.delete(instance) } -function createRecord( - id: string, - component: ComponentOptions | ClassComponent -): boolean { - if (!component) { - warn( - `HMR API usage is out of date.\n` + - `Please upgrade vue-loader/vite/rollup-plugin-vue or other relevant ` + - `dependency that handles Vue SFC compilation.` - ) - component = {} - } +function createRecord(id: string): boolean { if (map.has(id)) { return false } - map.set(id, { - component: isClassComponent(component) ? component.__vccOpts : component, - instances: new Set() - }) + map.set(id, new Set()) return true } +type HMRComponent = ComponentOptions | ClassComponent + +function normalizeClassComponent(component: HMRComponent): ComponentOptions { + return isClassComponent(component) ? component.__vccOpts : component +} + function rerender(id: string, newRender?: Function) { const record = map.get(id) - if (!record) return - if (newRender) record.component.render = newRender - // Array.from creates a snapshot which avoids the set being mutated during - // updates - Array.from(record.instances).forEach(instance => { + if (!record) { + return + } + // Create a snapshot which avoids the set being mutated during updates + ;[...record].forEach(instance => { if (newRender) { instance.render = newRender as InternalRenderFunction + normalizeClassComponent(instance.type as HMRComponent).render = newRender } instance.renderCache = [] // this flag forces child components with slot content to update @@ -104,40 +91,40 @@ function rerender(id: string, newRender?: Function) { }) } -function reload(id: string, newComp: ComponentOptions | ClassComponent) { +function reload(id: string, newComp: HMRComponent) { const record = map.get(id) if (!record) return - // Array.from creates a snapshot which avoids the set being mutated during - // updates - const { component, instances } = record - - if (!hmrDirtyComponents.has(component)) { - // 1. Update existing comp definition to match new one - newComp = isClassComponent(newComp) ? newComp.__vccOpts : newComp - extend(component, newComp) - for (const key in component) { - if (key !== '__file' && !(key in newComp)) { - delete (component as any)[key] + + newComp = normalizeClassComponent(newComp) + + // create a snapshot which avoids the set being mutated during updates + const instances = [...record] + + 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] + } } + // 2. mark definition dirty. This forces the renderer to replace the + // component on patch. + hmrDirtyComponents.add(oldComp) } - // 2. Mark component dirty. This forces the renderer to replace the component - // on patch. - hmrDirtyComponents.add(component) - // 3. Make sure to unmark the component after the reload. - queuePostFlushCb(() => { - hmrDirtyComponents.delete(component) - }) - } - Array.from(instances).forEach(instance => { - // invalidate options resolution cache + // 3. invalidate options resolution cache instance.appContext.optionsCache.delete(instance.type as any) + // 4. actually update if (instance.ceReload) { // custom element - hmrDirtyComponents.add(component) + hmrDirtyComponents.add(oldComp) instance.ceReload((newComp as any).styles) - hmrDirtyComponents.delete(component) + hmrDirtyComponents.delete(oldComp) } 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 @@ -162,6 +149,15 @@ function reload(id: string, newComp: ComponentOptions | ClassComponent) { '[HMR] Root or manually mounted instance modified. Full reload required.' ) } + } + + // 5. make sure to cleanup dirty hmr components after update + queuePostFlushCb(() => { + for (const instance of instances) { + hmrDirtyComponents.delete( + normalizeClassComponent(instance.type as HMRComponent) + ) + } }) }