Просмотр исходного кода

fix(hmr): force update cached slots during HMR

close #7155
close #7158
Evan You 3 лет назад
Родитель
Сommit
94fa67a4f7

+ 31 - 0
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(`<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`)
+  })
 })

+ 59 - 27
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,

+ 2 - 0
packages/runtime-core/src/componentPublicInstance.ts

@@ -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 (

+ 3 - 0
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<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.