diff --git a/packages/runtime-core/__tests__/hmr.spec.ts b/packages/runtime-core/__tests__/hmr.spec.ts index d1392b78465..b81a8b3af63 100644 --- a/packages/runtime-core/__tests__/hmr.spec.ts +++ b/packages/runtime-core/__tests__/hmr.spec.ts @@ -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(``) + } + createRecord(childId, Child) + + const Parent: ComponentOptions = { + __hmrId: parentId, + components: { Child }, + render: compileToFunction(`1`) + } + createRecord(parentId, Parent) + + render(h(Parent), root) + expect(serializeInner(root)).toBe(`1`) + + rerender(parentId, compileToFunction(`2`)) + expect(serializeInner(root)).toBe(`2`) + }) }) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 941231b393d..087e901354b 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -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,31 +930,57 @@ export function finishComponentSetup( } } -function createAttrsProxy(instance: ComponentInternalInstance): Data { - return new Proxy( - instance.attrs, - __DEV__ - ? { - get(target, key: string) { - markAttrsAccessed() - track(instance, TrackOpTypes.GET, '$attrs') - return target[key] - }, - set() { - warn(`setupContext.attrs is readonly.`) - return false - }, - deleteProperty() { - warn(`setupContext.attrs is readonly.`) - return false +function getAttrsProxy(instance: ComponentInternalInstance): Data { + return ( + instance.attrsProxy || + (instance.attrsProxy = new Proxy( + instance.attrs, + __DEV__ + ? { + get(target, key: string) { + markAttrsAccessed() + track(instance, TrackOpTypes.GET, '$attrs') + return target[key] + }, + set() { + warn(`setupContext.attrs is readonly.`) + return false + }, + deleteProperty() { + warn(`setupContext.attrs is readonly.`) + return false + } } - } - : { - get(target, key: string) { - track(instance, TrackOpTypes.GET, '$attrs') - return target[key] + : { + get(target, key: string) { + track(instance, TrackOpTypes.GET, '$attrs') + 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, diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 7b0ccf77ac9..dd2d29670e6 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -356,6 +356,8 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { 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 ( diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts index 81988599981..8f59099d833 100644 --- a/packages/runtime-core/src/componentSlots.ts +++ b/packages/runtime-core/src/componentSlots.ts @@ -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 = ( ...args: IfAny @@ -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.