fix(hmr): force update cached slots during HMR

close #7155
close #7158
This commit is contained in:
Evan You 2023-04-20 10:06:06 +08:00
parent 9b5a34bf8c
commit 94fa67a4f7
4 changed files with 95 additions and 27 deletions

View File

@ -537,4 +537,35 @@ describe('hot module replacement', () => {
render(h(Foo), root)
expect(serializeInner(root)).toBe('bar')
})
// #7155 - force HMR on slots content update
test('force update slot content change', () => {
const root = nodeOps.createElement('div')
const parentId = 'test-force-computed-parent'
const childId = 'test-force-computed-child'
const Child: ComponentOptions = {
__hmrId: childId,
computed: {
slotContent() {
return this.$slots.default?.()
}
},
render: compileToFunction(`<component :is="() => slotContent" />`)
}
createRecord(childId, Child)
const Parent: ComponentOptions = {
__hmrId: parentId,
components: { Child },
render: compileToFunction(`<Child>1</Child>`)
}
createRecord(parentId, Parent)
render(h(Parent), root)
expect(serializeInner(root)).toBe(`1`)
rerender(parentId, compileToFunction(`<Child>2</Child>`))
expect(serializeInner(root)).toBe(`2`)
})
})

View File

@ -349,6 +349,10 @@ export interface ComponentInternalInstance {
slots: InternalSlots
refs: Data
emit: EmitFn
attrsProxy: Data | null
slotsProxy: Slots | null
/**
* used for keeping track of .once event handlers on components
* @internal
@ -536,6 +540,9 @@ export function createComponentInstance(
setupState: EMPTY_OBJ,
setupContext: null,
attrsProxy: null,
slotsProxy: null,
// suspense related
suspense,
suspenseId: suspense ? suspense.pendingId : 0,
@ -923,8 +930,10 @@ export function finishComponentSetup(
}
}
function createAttrsProxy(instance: ComponentInternalInstance): Data {
return new Proxy(
function getAttrsProxy(instance: ComponentInternalInstance): Data {
return (
instance.attrsProxy ||
(instance.attrsProxy = new Proxy(
instance.attrs,
__DEV__
? {
@ -948,6 +957,30 @@ function createAttrsProxy(instance: ComponentInternalInstance): Data {
return target[key]
}
}
))
)
}
/**
* Dev-only
*/
function getSlotsProxy(instance: ComponentInternalInstance): Slots {
return (
instance.slotsProxy ||
(instance.slotsProxy = new Proxy(instance.slots, {
get(target, key: string) {
track(instance, TrackOpTypes.GET, '$slots')
return target[key]
},
set() {
warn(`setupContext.slots is readonly.`)
return false
},
deleteProperty() {
warn(`setupContext.slots is readonly.`)
return false
}
}))
)
}
@ -978,16 +1011,15 @@ export function createSetupContext(
instance.exposed = exposed || {}
}
let attrs: Data
if (__DEV__) {
// We use getters in dev in case libs like test-utils overwrite instance
// properties (overwrites should not be done in prod)
return Object.freeze({
get attrs() {
return attrs || (attrs = createAttrsProxy(instance))
return getAttrsProxy(instance)
},
get slots() {
return shallowReadonly(instance.slots)
return getSlotsProxy(instance)
},
get emit() {
return (event: string, ...args: any[]) => instance.emit(event, ...args)
@ -997,7 +1029,7 @@ export function createSetupContext(
} else {
return {
get attrs() {
return attrs || (attrs = createAttrsProxy(instance))
return getAttrsProxy(instance)
},
slots: instance.slots,
emit: instance.emit,

View File

@ -356,6 +356,8 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
if (key === '$attrs') {
track(instance, TrackOpTypes.GET, key)
__DEV__ && markAttrsAccessed()
} else if (__DEV__ && key === '$slots') {
track(instance, TrackOpTypes.GET, key)
}
return publicGetter(instance)
} else if (

View File

@ -23,6 +23,8 @@ import { ContextualRenderFn, withCtx } from './componentRenderContext'
import { isHmrUpdating } from './hmr'
import { DeprecationTypes, isCompatEnabled } from './compat/compatConfig'
import { toRaw } from '@vue/reactivity'
import { trigger } from '@vue/reactivity'
import { TriggerOpTypes } from '@vue/reactivity'
export type Slot<T extends any = any> = (
...args: IfAny<T, any[], [T] | (T extends undefined ? [] : never)>
@ -196,6 +198,7 @@ export const updateSlots = (
// Parent was HMR updated so slot content may have changed.
// force update slots and mark instance for hmr as well
extend(slots, children as Slots)
trigger(instance, TriggerOpTypes.SET, '$slots')
} else if (optimized && type === SlotFlags.STABLE) {
// compiled AND stable.
// no need to update, and skip stale slots removal.