diff --git a/packages/runtime-core/__tests__/hmr.spec.ts b/packages/runtime-core/__tests__/hmr.spec.ts
index 0a5821c8d..eaef8d401 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(``)
@@ -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 3bd5ef88b..3c3f5208b 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 {