fix(runtime-core): ensure setupContext.attrs reactivity when used in child slots

fix #4161
This commit is contained in:
Evan You 2021-07-21 17:31:00 -04:00
parent ff0c810300
commit 8560005601
2 changed files with 72 additions and 19 deletions

View File

@ -135,6 +135,44 @@ describe('api: setup context', () => {
expect(serializeInner(root)).toMatch(`<div class="baz"></div>`) 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 () => { it('context.slots', async () => {
const id = ref('foo') const id = ref('foo')

View File

@ -5,7 +5,9 @@ import {
shallowReadonly, shallowReadonly,
proxyRefs, proxyRefs,
EffectScope, EffectScope,
markRaw markRaw,
track,
TrackOpTypes
} from '@vue/reactivity' } from '@vue/reactivity'
import { import {
ComponentPublicInstance, ComponentPublicInstance,
@ -834,20 +836,33 @@ export function finishComponentSetup(
} }
} }
const attrDevProxyHandlers: ProxyHandler<Data> = { function createAttrsProxy(instance: ComponentInternalInstance): Data {
get: (target, key: string) => { return new Proxy(
instance.attrs,
__DEV__
? {
get(target, key: string) {
markAttrsAccessed() markAttrsAccessed()
track(instance, TrackOpTypes.GET, '$attrs')
return target[key] return target[key]
}, },
set: () => { set() {
warn(`setupContext.attrs is readonly.`) warn(`setupContext.attrs is readonly.`)
return false return false
}, },
deleteProperty: () => { deleteProperty() {
warn(`setupContext.attrs is readonly.`) warn(`setupContext.attrs is readonly.`)
return false return false
} }
} }
: {
get(target, key: string) {
track(instance, TrackOpTypes.GET, '$attrs')
return target[key]
}
}
)
}
export function createSetupContext( export function createSetupContext(
instance: ComponentInternalInstance instance: ComponentInternalInstance
@ -859,15 +874,13 @@ export function createSetupContext(
instance.exposed = exposed || {} instance.exposed = exposed || {}
} }
if (__DEV__) {
let attrs: Data let attrs: Data
if (__DEV__) {
// We use getters in dev in case libs like test-utils overwrite instance // We use getters in dev in case libs like test-utils overwrite instance
// properties (overwrites should not be done in prod) // properties (overwrites should not be done in prod)
return Object.freeze({ return Object.freeze({
get attrs() { get attrs() {
return ( return attrs || (attrs = createAttrsProxy(instance))
attrs || (attrs = new Proxy(instance.attrs, attrDevProxyHandlers))
)
}, },
get slots() { get slots() {
return shallowReadonly(instance.slots) return shallowReadonly(instance.slots)
@ -879,7 +892,9 @@ export function createSetupContext(
}) })
} else { } else {
return { return {
attrs: instance.attrs, get attrs() {
return attrs || (attrs = createAttrsProxy(instance))
},
slots: instance.slots, slots: instance.slots,
emit: instance.emit, emit: instance.emit,
expose expose