]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(runtime-core): ensure setupContext.attrs reactivity when used in child slots
authorEvan You <yyx990803@gmail.com>
Wed, 21 Jul 2021 21:31:00 +0000 (17:31 -0400)
committerEvan You <yyx990803@gmail.com>
Wed, 21 Jul 2021 21:31:00 +0000 (17:31 -0400)
fix #4161

packages/runtime-core/__tests__/apiSetupContext.spec.ts
packages/runtime-core/src/component.ts

index 3933c6f1129f0abfdbba1acd4ef53332b79676d6..eea75ac93a27b92068588f88a60450adf78b36fa 100644 (file)
@@ -135,6 +135,44 @@ describe('api: setup context', () => {
     expect(serializeInner(root)).toMatch(`<div class="baz"></div>`)
   })
 
+  // #4161
+  it('context.attrs in child component slots', async () => {
+    const toggle = ref(true)
+
+    const Parent = {
+      render: () => h(Child, toggle.value ? { id: 'foo' } : { class: 'baz' })
+    }
+
+    const Wrapper = {
+      render(this: any) {
+        return this.$slots.default()
+      }
+    }
+
+    const Child = {
+      inheritAttrs: false,
+      setup(_: any, { attrs }: any) {
+        return () => {
+          const vnode = h(Wrapper, null, {
+            default: () => [h('div', attrs)],
+            _: 1 // mark stable slots
+          })
+          vnode.dynamicChildren = [] // force optimized mode
+          return vnode
+        }
+      }
+    }
+
+    const root = nodeOps.createElement('div')
+    render(h(Parent), root)
+    expect(serializeInner(root)).toMatch(`<div id="foo"></div>`)
+
+    // should update even though it's not reactive
+    toggle.value = false
+    await nextTick()
+    expect(serializeInner(root)).toMatch(`<div class="baz"></div>`)
+  })
+
   it('context.slots', async () => {
     const id = ref('foo')
 
index 8c1d55a2163cae80b510622dc0c69bb2f83135ea..86c89c59883d2639780671e27a26607dc3ec2b40 100644 (file)
@@ -5,7 +5,9 @@ import {
   shallowReadonly,
   proxyRefs,
   EffectScope,
-  markRaw
+  markRaw,
+  track,
+  TrackOpTypes
 } from '@vue/reactivity'
 import {
   ComponentPublicInstance,
@@ -834,19 +836,32 @@ export function finishComponentSetup(
   }
 }
 
-const attrDevProxyHandlers: ProxyHandler<Data> = {
-  get: (target, key: string) => {
-    markAttrsAccessed()
-    return target[key]
-  },
-  set: () => {
-    warn(`setupContext.attrs is readonly.`)
-    return false
-  },
-  deleteProperty: () => {
-    warn(`setupContext.attrs is readonly.`)
-    return false
-  }
+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
+          }
+        }
+      : {
+          get(target, key: string) {
+            track(instance, TrackOpTypes.GET, '$attrs')
+            return target[key]
+          }
+        }
+  )
 }
 
 export function createSetupContext(
@@ -859,15 +874,13 @@ export function createSetupContext(
     instance.exposed = exposed || {}
   }
 
+  let attrs: Data
   if (__DEV__) {
-    let attrs: Data
     // 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 = new Proxy(instance.attrs, attrDevProxyHandlers))
-        )
+        return attrs || (attrs = createAttrsProxy(instance))
       },
       get slots() {
         return shallowReadonly(instance.slots)
@@ -879,7 +892,9 @@ export function createSetupContext(
     })
   } else {
     return {
-      attrs: instance.attrs,
+      get attrs() {
+        return attrs || (attrs = createAttrsProxy(instance))
+      },
       slots: instance.slots,
       emit: instance.emit,
       expose