]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(hmr): force update cached slots during HMR
authorEvan You <yyx990803@gmail.com>
Thu, 20 Apr 2023 02:06:06 +0000 (10:06 +0800)
committerEvan You <yyx990803@gmail.com>
Thu, 20 Apr 2023 02:06:06 +0000 (10:06 +0800)
close #7155
close #7158

packages/runtime-core/__tests__/hmr.spec.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentPublicInstance.ts
packages/runtime-core/src/componentSlots.ts

index d1392b78465a91f7eac309b53dda7491f9fd97c5..b81a8b3af63f5fac8f967f298f38432f9ecf2cf1 100644 (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`)
+  })
 })
index 941231b393dfbf217388dca6a1b48da5dc1d986d..087e901354b9951b8a1204fc450ec90f135ad274 100644 (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,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,
index 7b0ccf77ac99640224f9372533b61389e81e509a..dd2d29670e6a6fc939ce447e4bac55730e6bee9e 100644 (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 (
index 8198859998187f569331a3913e4fc039708f0b30..8f59099d8331fe558ce5da77719d7c4149abb4fa 100644 (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.