fix(hmr): fix updates for imported but not yet rendered components

This commit is contained in:
Evan You 2020-10-26 17:52:16 -04:00
parent 6b424c258f
commit 9c23ddf9c5
2 changed files with 50 additions and 39 deletions

View File

@ -33,9 +33,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 () => {
@ -47,7 +47,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,
@ -59,7 +59,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>`)
@ -125,7 +125,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)
@ -164,7 +164,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)
@ -209,7 +209,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(
@ -246,14 +246,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>`)
@ -272,14 +272,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>`)
@ -299,7 +299,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[] = []
@ -321,7 +321,7 @@ describe('hot module replacement', () => {
} }
} }
createRecord(parentId) createRecord(parentId, parentComp)
} }
const last = components[components.length - 1] const last = components[components.length - 1]

View File

@ -42,7 +42,10 @@ if (__DEV__ && (__BROWSER__ || __TEST__)) {
} as HMRRuntime } as HMRRuntime
} }
type HMRRecord = Set<ComponentInternalInstance> type HMRRecord = {
component: ComponentOptions
instances: Set<ComponentInternalInstance>
}
const map: Map<string, HMRRecord> = new Map() const map: Map<string, HMRRecord> = new Map()
@ -50,30 +53,37 @@ 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 ComponentOptions)
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,
component: ComponentOptions | ClassComponent
): boolean {
if (map.has(id)) { if (map.has(id)) {
return false return false
} }
map.set(id, new Set()) map.set(id, {
component: isClassComponent(component) ? component.__vccOpts : component,
instances: new Set()
})
return true return true
} }
function rerender(id: string, newRender?: Function) { function rerender(id: string, newRender?: Function) {
const record = map.get(id) const record = map.get(id)
if (!record) return if (!record) return
if (newRender) record.component.render = newRender
// Array.from creates a snapshot which avoids the set being mutated during // Array.from creates a snapshot which avoids the set being mutated during
// updates // updates
Array.from(record).forEach(instance => { Array.from(record.instances).forEach(instance => {
if (newRender) { if (newRender) {
instance.render = newRender as InternalRenderFunction instance.render = newRender as InternalRenderFunction
} }
@ -90,26 +100,27 @@ function reload(id: string, newComp: ComponentOptions | ClassComponent) {
if (!record) return if (!record) return
// Array.from creates a snapshot which avoids the set being mutated during // Array.from creates a snapshot which avoids the set being mutated during
// updates // updates
Array.from(record).forEach(instance => { const { component, instances } = record
const comp = instance.type
if (!hmrDirtyComponents.has(comp)) {
// 1. Update existing comp definition to match new one
newComp = isClassComponent(newComp) ? newComp.__vccOpts : newComp
extend(comp, newComp)
for (const key in comp) {
if (!(key in newComp)) {
delete (comp as any)[key]
}
}
// 2. Mark component dirty. This forces the renderer to replace the component
// on patch.
hmrDirtyComponents.add(comp)
// 3. Make sure to unmark the component after the reload.
queuePostFlushCb(() => {
hmrDirtyComponents.delete(comp)
})
}
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 in newComp)) {
delete (component as any)[key]
}
}
// 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 => {
if (instance.parent) { if (instance.parent) {
// 4. Force the parent instance to re-render. This will cause all updated // 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 // components to be unmounted and re-mounted. Queue the update so that we