mirror of https://github.com/vuejs/core.git
fix(hmr): hmr reload should work with async component (#11248)
This commit is contained in:
parent
1676f079b6
commit
c8b9794575
|
@ -29,6 +29,8 @@ function compileToFunction(template: string) {
|
||||||
return render
|
return render
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
|
||||||
|
|
||||||
describe('hot module replacement', () => {
|
describe('hot module replacement', () => {
|
||||||
test('inject global runtime', () => {
|
test('inject global runtime', () => {
|
||||||
expect(createRecord).toBeDefined()
|
expect(createRecord).toBeDefined()
|
||||||
|
@ -436,18 +438,23 @@ describe('hot module replacement', () => {
|
||||||
|
|
||||||
const Parent: ComponentOptions = {
|
const Parent: ComponentOptions = {
|
||||||
setup() {
|
setup() {
|
||||||
const com = ref()
|
const com1 = ref()
|
||||||
const changeRef = (value: any) => {
|
const changeRef1 = (value: any) => (com1.value = value)
|
||||||
com.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => [h(Child, { ref: changeRef }), com.value?.count]
|
const com2 = ref()
|
||||||
|
const changeRef2 = (value: any) => (com2.value = value)
|
||||||
|
|
||||||
|
return () => [
|
||||||
|
h(Child, { ref: changeRef1 }),
|
||||||
|
h(Child, { ref: changeRef2 }),
|
||||||
|
com1.value?.count,
|
||||||
|
]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
render(h(Parent), root)
|
render(h(Parent), root)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(serializeInner(root)).toBe(`<div>0</div>0`)
|
expect(serializeInner(root)).toBe(`<div>0</div><div>0</div>0`)
|
||||||
|
|
||||||
reload(childId, {
|
reload(childId, {
|
||||||
__hmrId: childId,
|
__hmrId: childId,
|
||||||
|
@ -458,9 +465,9 @@ describe('hot module replacement', () => {
|
||||||
render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
|
render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
|
||||||
})
|
})
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(serializeInner(root)).toBe(`<div>1</div>1`)
|
expect(serializeInner(root)).toBe(`<div>1</div><div>1</div>1`)
|
||||||
expect(unmountSpy).toHaveBeenCalledTimes(1)
|
expect(unmountSpy).toHaveBeenCalledTimes(2)
|
||||||
expect(mountSpy).toHaveBeenCalledTimes(1)
|
expect(mountSpy).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
// #1156 - static nodes should retain DOM element reference across updates
|
// #1156 - static nodes should retain DOM element reference across updates
|
||||||
|
@ -805,4 +812,43 @@ describe('hot module replacement', () => {
|
||||||
`<div><div>1<p>3</p></div></div><div><div>1<p>3</p></div></div><p>2</p>`,
|
`<div><div>1<p>3</p></div></div><div><div>1<p>3</p></div></div><p>2</p>`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// #11248
|
||||||
|
test('reload async component with multiple instances', async () => {
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
const childId = 'test-child-id'
|
||||||
|
const Child: ComponentOptions = {
|
||||||
|
__hmrId: childId,
|
||||||
|
data() {
|
||||||
|
return { count: 0 }
|
||||||
|
},
|
||||||
|
render: compileToFunction(`<div>{{ count }}</div>`),
|
||||||
|
}
|
||||||
|
const Comp = runtimeTest.defineAsyncComponent(() => Promise.resolve(Child))
|
||||||
|
const appId = 'test-app-id'
|
||||||
|
const App: ComponentOptions = {
|
||||||
|
__hmrId: appId,
|
||||||
|
render: () => [h(Comp), h(Comp)],
|
||||||
|
}
|
||||||
|
createRecord(appId, App)
|
||||||
|
|
||||||
|
render(h(App), root)
|
||||||
|
|
||||||
|
await timeout()
|
||||||
|
|
||||||
|
expect(serializeInner(root)).toBe(`<div>0</div><div>0</div>`)
|
||||||
|
|
||||||
|
// change count to 1
|
||||||
|
reload(childId, {
|
||||||
|
__hmrId: childId,
|
||||||
|
data() {
|
||||||
|
return { count: 1 }
|
||||||
|
},
|
||||||
|
render: compileToFunction(`<div>{{ count }}</div>`),
|
||||||
|
})
|
||||||
|
|
||||||
|
await timeout()
|
||||||
|
|
||||||
|
expect(serializeInner(root)).toBe(`<div>1</div><div>1</div>`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -14,7 +14,10 @@ type HMRComponent = ComponentOptions | ClassComponent
|
||||||
|
|
||||||
export let isHmrUpdating = false
|
export let isHmrUpdating = false
|
||||||
|
|
||||||
export const hmrDirtyComponents = new Set<ConcreteComponent>()
|
export const hmrDirtyComponents = new Map<
|
||||||
|
ConcreteComponent,
|
||||||
|
Set<ComponentInternalInstance>
|
||||||
|
>()
|
||||||
|
|
||||||
export interface HMRRuntime {
|
export interface HMRRuntime {
|
||||||
createRecord: typeof createRecord
|
createRecord: typeof createRecord
|
||||||
|
@ -110,18 +113,21 @@ function reload(id: string, newComp: HMRComponent) {
|
||||||
// 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.instances]
|
const instances = [...record.instances]
|
||||||
|
|
||||||
for (const instance of instances) {
|
for (let i = 0; i < instances.length; i++) {
|
||||||
|
const instance = instances[i]
|
||||||
const oldComp = normalizeClassComponent(instance.type as HMRComponent)
|
const oldComp = normalizeClassComponent(instance.type as HMRComponent)
|
||||||
|
|
||||||
if (!hmrDirtyComponents.has(oldComp)) {
|
let dirtyInstances = hmrDirtyComponents.get(oldComp)
|
||||||
|
if (!dirtyInstances) {
|
||||||
// 1. Update existing comp definition to match new one
|
// 1. Update existing comp definition to match new one
|
||||||
if (oldComp !== record.initialDef) {
|
if (oldComp !== record.initialDef) {
|
||||||
updateComponentDef(oldComp, newComp)
|
updateComponentDef(oldComp, newComp)
|
||||||
}
|
}
|
||||||
// 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.
|
||||||
hmrDirtyComponents.add(oldComp)
|
hmrDirtyComponents.set(oldComp, (dirtyInstances = new Set()))
|
||||||
}
|
}
|
||||||
|
dirtyInstances.add(instance)
|
||||||
|
|
||||||
// 3. invalidate options resolution cache
|
// 3. invalidate options resolution cache
|
||||||
instance.appContext.propsCache.delete(instance.type as any)
|
instance.appContext.propsCache.delete(instance.type as any)
|
||||||
|
@ -131,9 +137,9 @@ function reload(id: string, newComp: HMRComponent) {
|
||||||
// 4. actually update
|
// 4. actually update
|
||||||
if (instance.ceReload) {
|
if (instance.ceReload) {
|
||||||
// custom element
|
// custom element
|
||||||
hmrDirtyComponents.add(oldComp)
|
dirtyInstances.add(instance)
|
||||||
instance.ceReload((newComp as any).styles)
|
instance.ceReload((newComp as any).styles)
|
||||||
hmrDirtyComponents.delete(oldComp)
|
dirtyInstances.delete(instance)
|
||||||
} else if (instance.parent) {
|
} else 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
|
||||||
|
@ -141,8 +147,8 @@ function reload(id: string, newComp: HMRComponent) {
|
||||||
instance.parent.effect.dirty = true
|
instance.parent.effect.dirty = true
|
||||||
queueJob(() => {
|
queueJob(() => {
|
||||||
instance.parent!.update()
|
instance.parent!.update()
|
||||||
// #6930 avoid infinite recursion
|
// #6930, #11248 avoid infinite recursion
|
||||||
hmrDirtyComponents.delete(oldComp)
|
dirtyInstances.delete(instance)
|
||||||
})
|
})
|
||||||
} else if (instance.appContext.reload) {
|
} else if (instance.appContext.reload) {
|
||||||
// root instance mounted via createApp() has a reload method
|
// root instance mounted via createApp() has a reload method
|
||||||
|
@ -159,11 +165,7 @@ function reload(id: string, newComp: HMRComponent) {
|
||||||
|
|
||||||
// 5. make sure to cleanup dirty hmr components after update
|
// 5. make sure to cleanup dirty hmr components after update
|
||||||
queuePostFlushCb(() => {
|
queuePostFlushCb(() => {
|
||||||
for (const instance of instances) {
|
hmrDirtyComponents.clear()
|
||||||
hmrDirtyComponents.delete(
|
|
||||||
normalizeClassComponent(instance.type as HMRComponent),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -387,17 +387,16 @@ export function isVNode(value: any): value is VNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
|
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
|
||||||
if (
|
if (__DEV__ && n2.shapeFlag & ShapeFlags.COMPONENT && n1.component) {
|
||||||
__DEV__ &&
|
const dirtyInstances = hmrDirtyComponents.get(n2.type as ConcreteComponent)
|
||||||
n2.shapeFlag & ShapeFlags.COMPONENT &&
|
if (dirtyInstances && dirtyInstances.has(n1.component)) {
|
||||||
hmrDirtyComponents.has(n2.type as ConcreteComponent)
|
// #7042, ensure the vnode being unmounted during HMR
|
||||||
) {
|
// bitwise operations to remove keep alive flags
|
||||||
// #7042, ensure the vnode being unmounted during HMR
|
n1.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
|
||||||
// bitwise operations to remove keep alive flags
|
n2.shapeFlag &= ~ShapeFlags.COMPONENT_KEPT_ALIVE
|
||||||
n1.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
|
// HMR only: if the component has been hot-updated, force a reload.
|
||||||
n2.shapeFlag &= ~ShapeFlags.COMPONENT_KEPT_ALIVE
|
return false
|
||||||
// HMR only: if the component has been hot-updated, force a reload.
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
return n1.type === n2.type && n1.key === n2.key
|
return n1.type === n2.type && n1.key === n2.key
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue