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

View File

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

View File

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