Skip to content

Commit 94fa67a

Browse files
committed
fix(hmr): force update cached slots during HMR
close #7155 close #7158
1 parent 9b5a34b commit 94fa67a

File tree

4 files changed

+95
-27
lines changed

4 files changed

+95
-27
lines changed

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

+31
Original file line numberDiff line numberDiff line change
@@ -537,4 +537,35 @@ describe('hot module replacement', () => {
537537
render(h(Foo), root)
538538
expect(serializeInner(root)).toBe('bar')
539539
})
540+
541+
// #7155 - force HMR on slots content update
542+
test('force update slot content change', () => {
543+
const root = nodeOps.createElement('div')
544+
const parentId = 'test-force-computed-parent'
545+
const childId = 'test-force-computed-child'
546+
547+
const Child: ComponentOptions = {
548+
__hmrId: childId,
549+
computed: {
550+
slotContent() {
551+
return this.$slots.default?.()
552+
}
553+
},
554+
render: compileToFunction(`<component :is="() => slotContent" />`)
555+
}
556+
createRecord(childId, Child)
557+
558+
const Parent: ComponentOptions = {
559+
__hmrId: parentId,
560+
components: { Child },
561+
render: compileToFunction(`<Child>1</Child>`)
562+
}
563+
createRecord(parentId, Parent)
564+
565+
render(h(Parent), root)
566+
expect(serializeInner(root)).toBe(`1`)
567+
568+
rerender(parentId, compileToFunction(`<Child>2</Child>`))
569+
expect(serializeInner(root)).toBe(`2`)
570+
})
540571
})

packages/runtime-core/src/component.ts

+59-27
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,10 @@ export interface ComponentInternalInstance {
349349
slots: InternalSlots
350350
refs: Data
351351
emit: EmitFn
352+
353+
attrsProxy: Data | null
354+
slotsProxy: Slots | null
355+
352356
/**
353357
* used for keeping track of .once event handlers on components
354358
* @internal
@@ -536,6 +540,9 @@ export function createComponentInstance(
536540
setupState: EMPTY_OBJ,
537541
setupContext: null,
538542

543+
attrsProxy: null,
544+
slotsProxy: null,
545+
539546
// suspense related
540547
suspense,
541548
suspenseId: suspense ? suspense.pendingId : 0,
@@ -923,31 +930,57 @@ export function finishComponentSetup(
923930
}
924931
}
925932

926-
function createAttrsProxy(instance: ComponentInternalInstance): Data {
927-
return new Proxy(
928-
instance.attrs,
929-
__DEV__
930-
? {
931-
get(target, key: string) {
932-
markAttrsAccessed()
933-
track(instance, TrackOpTypes.GET, '$attrs')
934-
return target[key]
935-
},
936-
set() {
937-
warn(`setupContext.attrs is readonly.`)
938-
return false
939-
},
940-
deleteProperty() {
941-
warn(`setupContext.attrs is readonly.`)
942-
return false
933+
function getAttrsProxy(instance: ComponentInternalInstance): Data {
934+
return (
935+
instance.attrsProxy ||
936+
(instance.attrsProxy = new Proxy(
937+
instance.attrs,
938+
__DEV__
939+
? {
940+
get(target, key: string) {
941+
markAttrsAccessed()
942+
track(instance, TrackOpTypes.GET, '$attrs')
943+
return target[key]
944+
},
945+
set() {
946+
warn(`setupContext.attrs is readonly.`)
947+
return false
948+
},
949+
deleteProperty() {
950+
warn(`setupContext.attrs is readonly.`)
951+
return false
952+
}
943953
}
944-
}
945-
: {
946-
get(target, key: string) {
947-
track(instance, TrackOpTypes.GET, '$attrs')
948-
return target[key]
954+
: {
955+
get(target, key: string) {
956+
track(instance, TrackOpTypes.GET, '$attrs')
957+
return target[key]
958+
}
949959
}
950-
}
960+
))
961+
)
962+
}
963+
964+
/**
965+
* Dev-only
966+
*/
967+
function getSlotsProxy(instance: ComponentInternalInstance): Slots {
968+
return (
969+
instance.slotsProxy ||
970+
(instance.slotsProxy = new Proxy(instance.slots, {
971+
get(target, key: string) {
972+
track(instance, TrackOpTypes.GET, '$slots')
973+
return target[key]
974+
},
975+
set() {
976+
warn(`setupContext.slots is readonly.`)
977+
return false
978+
},
979+
deleteProperty() {
980+
warn(`setupContext.slots is readonly.`)
981+
return false
982+
}
983+
}))
951984
)
952985
}
953986

@@ -978,16 +1011,15 @@ export function createSetupContext(
9781011
instance.exposed = exposed || {}
9791012
}
9801013

981-
let attrs: Data
9821014
if (__DEV__) {
9831015
// We use getters in dev in case libs like test-utils overwrite instance
9841016
// properties (overwrites should not be done in prod)
9851017
return Object.freeze({
9861018
get attrs() {
987-
return attrs || (attrs = createAttrsProxy(instance))
1019+
return getAttrsProxy(instance)
9881020
},
9891021
get slots() {
990-
return shallowReadonly(instance.slots)
1022+
return getSlotsProxy(instance)
9911023
},
9921024
get emit() {
9931025
return (event: string, ...args: any[]) => instance.emit(event, ...args)
@@ -997,7 +1029,7 @@ export function createSetupContext(
9971029
} else {
9981030
return {
9991031
get attrs() {
1000-
return attrs || (attrs = createAttrsProxy(instance))
1032+
return getAttrsProxy(instance)
10011033
},
10021034
slots: instance.slots,
10031035
emit: instance.emit,

packages/runtime-core/src/componentPublicInstance.ts

+2
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,8 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
356356
if (key === '$attrs') {
357357
track(instance, TrackOpTypes.GET, key)
358358
__DEV__ && markAttrsAccessed()
359+
} else if (__DEV__ && key === '$slots') {
360+
track(instance, TrackOpTypes.GET, key)
359361
}
360362
return publicGetter(instance)
361363
} else if (

packages/runtime-core/src/componentSlots.ts

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { ContextualRenderFn, withCtx } from './componentRenderContext'
2323
import { isHmrUpdating } from './hmr'
2424
import { DeprecationTypes, isCompatEnabled } from './compat/compatConfig'
2525
import { toRaw } from '@vue/reactivity'
26+
import { trigger } from '@vue/reactivity'
27+
import { TriggerOpTypes } from '@vue/reactivity'
2628

2729
export type Slot<T extends any = any> = (
2830
...args: IfAny<T, any[], [T] | (T extends undefined ? [] : never)>
@@ -196,6 +198,7 @@ export const updateSlots = (
196198
// Parent was HMR updated so slot content may have changed.
197199
// force update slots and mark instance for hmr as well
198200
extend(slots, children as Slots)
201+
trigger(instance, TriggerOpTypes.SET, '$slots')
199202
} else if (optimized && type === SlotFlags.STABLE) {
200203
// compiled AND stable.
201204
// no need to update, and skip stale slots removal.

0 commit comments

Comments
 (0)