fix(hmr): fix hmr for components with no active instance yet

fix #4757
This commit is contained in:
Evan You 2021-10-08 12:39:24 -04:00
parent 6bcb7a5ea3
commit 9e3d7731c7
2 changed files with 91 additions and 30 deletions

View File

@ -36,9 +36,9 @@ describe('hot module replacement', () => {
}) })
test('createRecord', () => { test('createRecord', () => {
expect(createRecord('test1')).toBe(true) expect(createRecord('test1', {})).toBe(true)
// if id has already been created, should return false // if id has already been created, should return false
expect(createRecord('test1')).toBe(false) expect(createRecord('test1', {})).toBe(false)
}) })
test('rerender', async () => { test('rerender', async () => {
@ -50,7 +50,7 @@ describe('hot module replacement', () => {
__hmrId: childId, __hmrId: childId,
render: compileToFunction(`<div><slot/></div>`) render: compileToFunction(`<div><slot/></div>`)
} }
createRecord(childId) createRecord(childId, Child)
const Parent: ComponentOptions = { const Parent: ComponentOptions = {
__hmrId: parentId, __hmrId: parentId,
@ -62,7 +62,7 @@ describe('hot module replacement', () => {
`<div @click="count++">{{ count }}<Child>{{ count }}</Child></div>` `<div @click="count++">{{ count }}<Child>{{ count }}</Child></div>`
) )
} }
createRecord(parentId) createRecord(parentId, Parent)
render(h(Parent), root) render(h(Parent), root)
expect(serializeInner(root)).toBe(`<div>0<div>0</div></div>`) expect(serializeInner(root)).toBe(`<div>0<div>0</div></div>`)
@ -128,7 +128,7 @@ describe('hot module replacement', () => {
unmounted: unmountSpy, unmounted: unmountSpy,
render: compileToFunction(`<div @click="count++">{{ count }}</div>`) render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
} }
createRecord(childId) createRecord(childId, Child)
const Parent: ComponentOptions = { const Parent: ComponentOptions = {
render: () => h(Child) render: () => h(Child)
@ -167,7 +167,7 @@ describe('hot module replacement', () => {
render: compileToFunction(`<div @click="count++">{{ count }}</div>`) render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
} }
} }
createRecord(childId) createRecord(childId, Child)
const Parent: ComponentOptions = { const Parent: ComponentOptions = {
render: () => h(Child) render: () => h(Child)
@ -212,7 +212,7 @@ describe('hot module replacement', () => {
}, },
render: compileToFunction(template) render: compileToFunction(template)
} }
createRecord(id) createRecord(id, Comp)
render(h(Comp), root) render(h(Comp), root)
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(
@ -249,14 +249,14 @@ describe('hot module replacement', () => {
}, },
render: compileToFunction(`<div>{{ msg }}</div>`) render: compileToFunction(`<div>{{ msg }}</div>`)
} }
createRecord(childId) createRecord(childId, Child)
const Parent: ComponentOptions = { const Parent: ComponentOptions = {
__hmrId: parentId, __hmrId: parentId,
components: { Child }, components: { Child },
render: compileToFunction(`<Child msg="foo" />`) render: compileToFunction(`<Child msg="foo" />`)
} }
createRecord(parentId) createRecord(parentId, Parent)
render(h(Parent), root) render(h(Parent), root)
expect(serializeInner(root)).toBe(`<div>foo</div>`) expect(serializeInner(root)).toBe(`<div>foo</div>`)
@ -275,14 +275,14 @@ describe('hot module replacement', () => {
__hmrId: childId, __hmrId: childId,
render: compileToFunction(`<div>child</div>`) render: compileToFunction(`<div>child</div>`)
} }
createRecord(childId) createRecord(childId, Child)
const Parent: ComponentOptions = { const Parent: ComponentOptions = {
__hmrId: parentId, __hmrId: parentId,
components: { Child }, components: { Child },
render: compileToFunction(`<Child class="test" />`) render: compileToFunction(`<Child class="test" />`)
} }
createRecord(parentId) createRecord(parentId, Parent)
render(h(Parent), root) render(h(Parent), root)
expect(serializeInner(root)).toBe(`<div class="test">child</div>`) expect(serializeInner(root)).toBe(`<div class="test">child</div>`)
@ -302,7 +302,7 @@ describe('hot module replacement', () => {
__hmrId: childId, __hmrId: childId,
render: compileToFunction(`<div>child</div>`) render: compileToFunction(`<div>child</div>`)
} }
createRecord(childId) createRecord(childId, Child)
const components: ComponentOptions[] = [] const components: ComponentOptions[] = []
@ -324,7 +324,7 @@ describe('hot module replacement', () => {
} }
} }
createRecord(parentId) createRecord(parentId, parentComp)
} }
const last = components[components.length - 1] const last = components[components.length - 1]
@ -370,7 +370,7 @@ describe('hot module replacement', () => {
</Child> </Child>
`) `)
} }
createRecord(parentId) createRecord(parentId, Parent)
render(h(Parent), root) render(h(Parent), root)
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(
@ -410,7 +410,7 @@ describe('hot module replacement', () => {
return h('div') return h('div')
} }
} }
createRecord(childId) createRecord(childId, Child)
const Parent: ComponentOptions = { const Parent: ComponentOptions = {
render: () => h(Child) render: () => h(Child)
@ -435,4 +435,38 @@ describe('hot module replacement', () => {
expect(createSpy1).toHaveBeenCalledTimes(1) expect(createSpy1).toHaveBeenCalledTimes(1)
expect(createSpy2).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')
})
}) })

View File

@ -10,6 +10,8 @@ import {
import { queueJob, queuePostFlushCb } from './scheduler' import { queueJob, queuePostFlushCb } from './scheduler'
import { extend, getGlobalThis } from '@vue/shared' import { extend, getGlobalThis } from '@vue/shared'
type HMRComponent = ComponentOptions | ClassComponent
export let isHmrUpdating = false export let isHmrUpdating = false
export const hmrDirtyComponents = new Set<ConcreteComponent>() export const hmrDirtyComponents = new Set<ConcreteComponent>()
@ -33,32 +35,42 @@ if (__DEV__) {
} as HMRRuntime } 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) { export function registerHMR(instance: ComponentInternalInstance) {
const id = instance.type.__hmrId! const id = instance.type.__hmrId!
let record = map.get(id) let record = map.get(id)
if (!record) { if (!record) {
createRecord(id) createRecord(id, instance.type as HMRComponent)
record = map.get(id)! record = map.get(id)!
} }
record.add(instance) record.instances.add(instance)
} }
export function unregisterHMR(instance: ComponentInternalInstance) { 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)) { if (map.has(id)) {
return false return false
} }
map.set(id, new Set()) map.set(id, {
initialDef: normalizeClassComponent(initialDef),
instances: new Set()
})
return true return true
} }
type HMRComponent = ComponentOptions | ClassComponent
function normalizeClassComponent(component: HMRComponent): ComponentOptions { function normalizeClassComponent(component: HMRComponent): ComponentOptions {
return isClassComponent(component) ? component.__vccOpts : component return isClassComponent(component) ? component.__vccOpts : component
} }
@ -68,8 +80,12 @@ function rerender(id: string, newRender?: Function) {
if (!record) { if (!record) {
return return
} }
// update initial record (for not-yet-rendered component)
record.initialDef.render = newRender
// Create a snapshot which avoids the set being mutated during updates // Create a snapshot which avoids the set being mutated during updates
;[...record].forEach(instance => { ;[...record.instances].forEach(instance => {
if (newRender) { if (newRender) {
instance.render = newRender as InternalRenderFunction instance.render = newRender as InternalRenderFunction
normalizeClassComponent(instance.type as HMRComponent).render = newRender normalizeClassComponent(instance.type as HMRComponent).render = newRender
@ -87,20 +103,19 @@ function reload(id: string, newComp: HMRComponent) {
if (!record) return if (!record) return
newComp = normalizeClassComponent(newComp) 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 // create a snapshot which avoids the set being mutated during updates
const instances = [...record] const instances = [...record.instances]
for (const instance of instances) { for (const instance of instances) {
const oldComp = normalizeClassComponent(instance.type as HMRComponent) const oldComp = normalizeClassComponent(instance.type as HMRComponent)
if (!hmrDirtyComponents.has(oldComp)) { if (!hmrDirtyComponents.has(oldComp)) {
// 1. Update existing comp definition to match new one // 1. Update existing comp definition to match new one
extend(oldComp, newComp) if (oldComp !== record.initialDef) {
for (const key in oldComp) { updateComponentDef(oldComp, newComp)
if (key !== '__file' && !(key in newComp)) {
delete (oldComp as any)[key]
}
} }
// 2. mark definition dirty. This forces the renderer to replace the // 2. mark definition dirty. This forces the renderer to replace the
// component on patch. // 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 { function tryWrap(fn: (id: string, arg: any) => any): Function {
return (id: string, arg: any) => { return (id: string, arg: any) => {
try { try {