Skip to content

Commit 9e3d773

Browse files
committed
fix(hmr): fix hmr for components with no active instance yet
fix #4757
1 parent 6bcb7a5 commit 9e3d773

File tree

2 files changed

+91
-30
lines changed

2 files changed

+91
-30
lines changed

packages/runtime-core/__tests__/hmr.spec.ts

+49-15
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ describe('hot module replacement', () => {
3636
})
3737

3838
test('createRecord', () => {
39-
expect(createRecord('test1')).toBe(true)
39+
expect(createRecord('test1', {})).toBe(true)
4040
// if id has already been created, should return false
41-
expect(createRecord('test1')).toBe(false)
41+
expect(createRecord('test1', {})).toBe(false)
4242
})
4343

4444
test('rerender', async () => {
@@ -50,7 +50,7 @@ describe('hot module replacement', () => {
5050
__hmrId: childId,
5151
render: compileToFunction(`<div><slot/></div>`)
5252
}
53-
createRecord(childId)
53+
createRecord(childId, Child)
5454

5555
const Parent: ComponentOptions = {
5656
__hmrId: parentId,
@@ -62,7 +62,7 @@ describe('hot module replacement', () => {
6262
`<div @click="count++">{{ count }}<Child>{{ count }}</Child></div>`
6363
)
6464
}
65-
createRecord(parentId)
65+
createRecord(parentId, Parent)
6666

6767
render(h(Parent), root)
6868
expect(serializeInner(root)).toBe(`<div>0<div>0</div></div>`)
@@ -128,7 +128,7 @@ describe('hot module replacement', () => {
128128
unmounted: unmountSpy,
129129
render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
130130
}
131-
createRecord(childId)
131+
createRecord(childId, Child)
132132

133133
const Parent: ComponentOptions = {
134134
render: () => h(Child)
@@ -167,7 +167,7 @@ describe('hot module replacement', () => {
167167
render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
168168
}
169169
}
170-
createRecord(childId)
170+
createRecord(childId, Child)
171171

172172
const Parent: ComponentOptions = {
173173
render: () => h(Child)
@@ -212,7 +212,7 @@ describe('hot module replacement', () => {
212212
},
213213
render: compileToFunction(template)
214214
}
215-
createRecord(id)
215+
createRecord(id, Comp)
216216

217217
render(h(Comp), root)
218218
expect(serializeInner(root)).toBe(
@@ -249,14 +249,14 @@ describe('hot module replacement', () => {
249249
},
250250
render: compileToFunction(`<div>{{ msg }}</div>`)
251251
}
252-
createRecord(childId)
252+
createRecord(childId, Child)
253253

254254
const Parent: ComponentOptions = {
255255
__hmrId: parentId,
256256
components: { Child },
257257
render: compileToFunction(`<Child msg="foo" />`)
258258
}
259-
createRecord(parentId)
259+
createRecord(parentId, Parent)
260260

261261
render(h(Parent), root)
262262
expect(serializeInner(root)).toBe(`<div>foo</div>`)
@@ -275,14 +275,14 @@ describe('hot module replacement', () => {
275275
__hmrId: childId,
276276
render: compileToFunction(`<div>child</div>`)
277277
}
278-
createRecord(childId)
278+
createRecord(childId, Child)
279279

280280
const Parent: ComponentOptions = {
281281
__hmrId: parentId,
282282
components: { Child },
283283
render: compileToFunction(`<Child class="test" />`)
284284
}
285-
createRecord(parentId)
285+
createRecord(parentId, Parent)
286286

287287
render(h(Parent), root)
288288
expect(serializeInner(root)).toBe(`<div class="test">child</div>`)
@@ -302,7 +302,7 @@ describe('hot module replacement', () => {
302302
__hmrId: childId,
303303
render: compileToFunction(`<div>child</div>`)
304304
}
305-
createRecord(childId)
305+
createRecord(childId, Child)
306306

307307
const components: ComponentOptions[] = []
308308

@@ -324,7 +324,7 @@ describe('hot module replacement', () => {
324324
}
325325
}
326326

327-
createRecord(parentId)
327+
createRecord(parentId, parentComp)
328328
}
329329

330330
const last = components[components.length - 1]
@@ -370,7 +370,7 @@ describe('hot module replacement', () => {
370370
</Child>
371371
`)
372372
}
373-
createRecord(parentId)
373+
createRecord(parentId, Parent)
374374

375375
render(h(Parent), root)
376376
expect(serializeInner(root)).toBe(
@@ -410,7 +410,7 @@ describe('hot module replacement', () => {
410410
return h('div')
411411
}
412412
}
413-
createRecord(childId)
413+
createRecord(childId, Child)
414414

415415
const Parent: ComponentOptions = {
416416
render: () => h(Child)
@@ -435,4 +435,38 @@ describe('hot module replacement', () => {
435435
expect(createSpy1).toHaveBeenCalledTimes(1)
436436
expect(createSpy2).toHaveBeenCalledTimes(1)
437437
})
438+
439+
// #4757
440+
test('rerender for component that has no active instance yet', () => {
441+
const id = 'no-active-instance-rerender'
442+
const Foo: ComponentOptions = {
443+
__hmrId: id,
444+
render: () => 'foo'
445+
}
446+
447+
createRecord(id, Foo)
448+
rerender(id, () => 'bar')
449+
450+
const root = nodeOps.createElement('div')
451+
render(h(Foo), root)
452+
expect(serializeInner(root)).toBe('bar')
453+
})
454+
455+
test('reload for component that has no active instance yet', () => {
456+
const id = 'no-active-instance-reload'
457+
const Foo: ComponentOptions = {
458+
__hmrId: id,
459+
render: () => 'foo'
460+
}
461+
462+
createRecord(id, Foo)
463+
reload(id, {
464+
__hmrId: id,
465+
render: () => 'bar'
466+
})
467+
468+
const root = nodeOps.createElement('div')
469+
render(h(Foo), root)
470+
expect(serializeInner(root)).toBe('bar')
471+
})
438472
})

packages/runtime-core/src/hmr.ts

+42-15
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
import { queueJob, queuePostFlushCb } from './scheduler'
1111
import { extend, getGlobalThis } from '@vue/shared'
1212

13+
type HMRComponent = ComponentOptions | ClassComponent
14+
1315
export let isHmrUpdating = false
1416

1517
export const hmrDirtyComponents = new Set<ConcreteComponent>()
@@ -33,32 +35,42 @@ if (__DEV__) {
3335
} as HMRRuntime
3436
}
3537

36-
const map: Map<string, Set<ComponentInternalInstance>> = new Map()
38+
const map: Map<
39+
string,
40+
{
41+
// the initial component definition is recorded on import - this allows us
42+
// to apply hot updates to the component even when there are no actively
43+
// rendered instance.
44+
initialDef: ComponentOptions
45+
instances: Set<ComponentInternalInstance>
46+
}
47+
> = new Map()
3748

3849
export function registerHMR(instance: ComponentInternalInstance) {
3950
const id = instance.type.__hmrId!
4051
let record = map.get(id)
4152
if (!record) {
42-
createRecord(id)
53+
createRecord(id, instance.type as HMRComponent)
4354
record = map.get(id)!
4455
}
45-
record.add(instance)
56+
record.instances.add(instance)
4657
}
4758

4859
export function unregisterHMR(instance: ComponentInternalInstance) {
49-
map.get(instance.type.__hmrId!)!.delete(instance)
60+
map.get(instance.type.__hmrId!)!.instances.delete(instance)
5061
}
5162

52-
function createRecord(id: string): boolean {
63+
function createRecord(id: string, initialDef: HMRComponent): boolean {
5364
if (map.has(id)) {
5465
return false
5566
}
56-
map.set(id, new Set())
67+
map.set(id, {
68+
initialDef: normalizeClassComponent(initialDef),
69+
instances: new Set()
70+
})
5771
return true
5872
}
5973

60-
type HMRComponent = ComponentOptions | ClassComponent
61-
6274
function normalizeClassComponent(component: HMRComponent): ComponentOptions {
6375
return isClassComponent(component) ? component.__vccOpts : component
6476
}
@@ -68,8 +80,12 @@ function rerender(id: string, newRender?: Function) {
6880
if (!record) {
6981
return
7082
}
83+
84+
// update initial record (for not-yet-rendered component)
85+
record.initialDef.render = newRender
86+
7187
// Create a snapshot which avoids the set being mutated during updates
72-
;[...record].forEach(instance => {
88+
;[...record.instances].forEach(instance => {
7389
if (newRender) {
7490
instance.render = newRender as InternalRenderFunction
7591
normalizeClassComponent(instance.type as HMRComponent).render = newRender
@@ -87,20 +103,19 @@ function reload(id: string, newComp: HMRComponent) {
87103
if (!record) return
88104

89105
newComp = normalizeClassComponent(newComp)
106+
// update initial def (for not-yet-rendered components)
107+
updateComponentDef(record.initialDef, newComp)
90108

91109
// create a snapshot which avoids the set being mutated during updates
92-
const instances = [...record]
110+
const instances = [...record.instances]
93111

94112
for (const instance of instances) {
95113
const oldComp = normalizeClassComponent(instance.type as HMRComponent)
96114

97115
if (!hmrDirtyComponents.has(oldComp)) {
98116
// 1. Update existing comp definition to match new one
99-
extend(oldComp, newComp)
100-
for (const key in oldComp) {
101-
if (key !== '__file' && !(key in newComp)) {
102-
delete (oldComp as any)[key]
103-
}
117+
if (oldComp !== record.initialDef) {
118+
updateComponentDef(oldComp, newComp)
104119
}
105120
// 2. mark definition dirty. This forces the renderer to replace the
106121
// component on patch.
@@ -152,6 +167,18 @@ function reload(id: string, newComp: HMRComponent) {
152167
})
153168
}
154169

170+
function updateComponentDef(
171+
oldComp: ComponentOptions,
172+
newComp: ComponentOptions
173+
) {
174+
extend(oldComp, newComp)
175+
for (const key in oldComp) {
176+
if (key !== '__file' && !(key in newComp)) {
177+
delete (oldComp as any)[key]
178+
}
179+
}
180+
}
181+
155182
function tryWrap(fn: (id: string, arg: any) => any): Function {
156183
return (id: string, arg: any) => {
157184
try {

0 commit comments

Comments
 (0)