Skip to content

Commit

Permalink
fix(hmr): handle possible duplicate component definitions with same id
Browse files Browse the repository at this point in the history
fixes regression in vitepress
  • Loading branch information
yyx990803 committed Sep 8, 2021
1 parent 96b531b commit aa8908a
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 70 deletions.
30 changes: 15 additions & 15 deletions packages/runtime-core/__tests__/hmr.spec.ts
Expand Up @@ -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 () => {
Expand All @@ -50,7 +50,7 @@ describe('hot module replacement', () => {
__hmrId: childId,
render: compileToFunction(`<div><slot/></div>`)
}
createRecord(childId, Child)
createRecord(childId)

const Parent: ComponentOptions = {
__hmrId: parentId,
Expand All @@ -62,7 +62,7 @@ describe('hot module replacement', () => {
`<div @click="count++">{{ count }}<Child>{{ count }}</Child></div>`
)
}
createRecord(parentId, Parent)
createRecord(parentId)

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

const Parent: ComponentOptions = {
render: () => h(Child)
Expand Down Expand Up @@ -167,7 +167,7 @@ describe('hot module replacement', () => {
render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
}
}
createRecord(childId, Child)
createRecord(childId)

const Parent: ComponentOptions = {
render: () => h(Child)
Expand Down Expand Up @@ -212,7 +212,7 @@ describe('hot module replacement', () => {
},
render: compileToFunction(template)
}
createRecord(id, Comp)
createRecord(id)

render(h(Comp), root)
expect(serializeInner(root)).toBe(
Expand Down Expand Up @@ -249,14 +249,14 @@ describe('hot module replacement', () => {
},
render: compileToFunction(`<div>{{ msg }}</div>`)
}
createRecord(childId, Child)
createRecord(childId)

const Parent: ComponentOptions = {
__hmrId: parentId,
components: { Child },
render: compileToFunction(`<Child msg="foo" />`)
}
createRecord(parentId, Parent)
createRecord(parentId)

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

const Parent: ComponentOptions = {
__hmrId: parentId,
components: { Child },
render: compileToFunction(`<Child class="test" />`)
}
createRecord(parentId, Parent)
createRecord(parentId)

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

const components: ComponentOptions[] = []

Expand All @@ -324,7 +324,7 @@ describe('hot module replacement', () => {
}
}

createRecord(parentId, parentComp)
createRecord(parentId)
}

const last = components[components.length - 1]
Expand Down Expand Up @@ -370,7 +370,7 @@ describe('hot module replacement', () => {
</Child>
`)
}
createRecord(parentId, Parent)
createRecord(parentId)

render(h(Parent), root)
expect(serializeInner(root)).toBe(
Expand Down Expand Up @@ -410,7 +410,7 @@ describe('hot module replacement', () => {
return h('div')
}
}
createRecord(childId, Child)
createRecord(childId)

const Parent: ComponentOptions = {
render: () => h(Child)
Expand Down
106 changes: 51 additions & 55 deletions packages/runtime-core/src/hmr.ts
Expand Up @@ -9,7 +9,6 @@ import {
} from './component'
import { queueJob, queuePostFlushCb } from './scheduler'
import { extend } from '@vue/shared'
import { warn } from './warning'

export let isHmrUpdating = false

Expand Down Expand Up @@ -43,58 +42,46 @@ if (__DEV__) {
} as HMRRuntime
}

type HMRRecord = {
component: ComponentOptions
instances: Set<ComponentInternalInstance>
}

const map: Map<string, HMRRecord> = new Map()
const map: Map<string, Set<ComponentInternalInstance>> = new Map()

export function registerHMR(instance: ComponentInternalInstance) {
const id = instance.type.__hmrId!
let record = map.get(id)
if (!record) {
createRecord(id, instance.type as ComponentOptions)
createRecord(id)
record = map.get(id)!
}
record.instances.add(instance)
record.add(instance)
}

export function unregisterHMR(instance: ComponentInternalInstance) {
map.get(instance.type.__hmrId!)!.instances.delete(instance)
map.get(instance.type.__hmrId!)!.delete(instance)
}

function createRecord(
id: string,
component: ComponentOptions | ClassComponent
): boolean {
if (!component) {
warn(
`HMR API usage is out of date.\n` +
`Please upgrade vue-loader/vite/rollup-plugin-vue or other relevant ` +
`dependency that handles Vue SFC compilation.`
)
component = {}
}
function createRecord(id: string): boolean {
if (map.has(id)) {
return false
}
map.set(id, {
component: isClassComponent(component) ? component.__vccOpts : component,
instances: new Set()
})
map.set(id, new Set())
return true
}

type HMRComponent = ComponentOptions | ClassComponent

function normalizeClassComponent(component: HMRComponent): ComponentOptions {
return isClassComponent(component) ? component.__vccOpts : component
}

function rerender(id: string, newRender?: Function) {
const record = map.get(id)
if (!record) return
if (newRender) record.component.render = newRender
// Array.from creates a snapshot which avoids the set being mutated during
// updates
Array.from(record.instances).forEach(instance => {
if (!record) {
return
}
// Create a snapshot which avoids the set being mutated during updates
;[...record].forEach(instance => {
if (newRender) {
instance.render = newRender as InternalRenderFunction
normalizeClassComponent(instance.type as HMRComponent).render = newRender
}
instance.renderCache = []
// this flag forces child components with slot content to update
Expand All @@ -104,40 +91,40 @@ function rerender(id: string, newRender?: Function) {
})
}

function reload(id: string, newComp: ComponentOptions | ClassComponent) {
function reload(id: string, newComp: HMRComponent) {
const record = map.get(id)
if (!record) return
// Array.from creates a snapshot which avoids the set being mutated during
// updates
const { component, instances } = record

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 !== '__file' && !(key in newComp)) {
delete (component as any)[key]

newComp = normalizeClassComponent(newComp)

// create a snapshot which avoids the set being mutated during updates
const instances = [...record]

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]
}
}
// 2. mark definition dirty. This forces the renderer to replace the
// component on patch.
hmrDirtyComponents.add(oldComp)
}
// 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 => {
// invalidate options resolution cache
// 3. invalidate options resolution cache
instance.appContext.optionsCache.delete(instance.type as any)

// 4. actually update
if (instance.ceReload) {
// custom element
hmrDirtyComponents.add(component)
hmrDirtyComponents.add(oldComp)
instance.ceReload((newComp as any).styles)
hmrDirtyComponents.delete(component)
hmrDirtyComponents.delete(oldComp)
} else if (instance.parent) {
// 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
Expand All @@ -162,6 +149,15 @@ function reload(id: string, newComp: ComponentOptions | ClassComponent) {
'[HMR] Root or manually mounted instance modified. Full reload required.'
)
}
}

// 5. make sure to cleanup dirty hmr components after update
queuePostFlushCb(() => {
for (const instance of instances) {
hmrDirtyComponents.delete(
normalizeClassComponent(instance.type as HMRComponent)
)
}
})
}

Expand Down

0 comments on commit aa8908a

Please sign in to comment.