通过组件实例代理统一访问 setup 状态、props 和组件公共属性
2025-10-29 11:35:33
上一节中,组件已经可以接收 props 和 attrs。
在组件的 render 中,通常可以直接通过 this 访问组件状态和公共属性:
import { createApp, h, ref } from '../dist/vue.esm.js'
const Com = {
props: ['msg'],
setup() {
const count = ref(0)
return {
count
}
},
render() {
return h(
'div',
{
onClick: () => {
this.count++
}
},
`${this.msg}: ${this.count}, ${this.$attrs.count}`
)
}
}
createApp(Com, {
msg: '消息',
count: 1
}).mount('#app')
这里的 this 并不是组件实例本身,而是一个代理对象。
代理对象会把不同来源的数据统一暴露出来:
this.count:访问setup返回的状态this.msg:访问组件的propsthis.$attrs:访问组件未声明的参数this.$slots:访问组件插槽this.$refs:访问模板引用
接下来实现这个代理对象。
实现代理对象
1. 保存组件上下文
在组件实例上增加 ctx 和 proxy:
// packages/runtime-core/src/component.ts
export function createComponentInstance(vnode) {
const { type } = vnode
const instance = {
// ...
proxy: null,
ctx: null
}
instance.ctx = {
_: instance
}
return instance
}
2. 创建组件代理
上一节的 setupComponent 同时负责初始化 props 和执行 setup。
现在把执行 setup 的逻辑抽离出来,并在这里创建代理:
// packages/runtime-core/src/component.ts
import { proxyRefs } from '@vue/reactivity'
import { isFunction, isObject } from '@vue/shared'
import { initProps } from './componentProps'
import { publicInstanceProxyHandlers } from './componentPublicInstance'
export function setupComponent(instance) {
initProps(instance)
setupStatefulComponent(instance)
}
function setupStatefulComponent(instance) {
const { type } = instance
instance.proxy = new Proxy(instance.ctx, publicInstanceProxyHandlers)
if (isFunction(type.setup)) {
const setupContext = createSetupContext(instance)
const setupResult = type.setup(instance.props, setupContext)
if (isFunction(setupResult)) {
instance.render = setupResult
}
else if (isObject(setupResult)) {
instance.setupState = proxyRefs(setupResult)
}
}
if (!instance.render) {
instance.render = type.render
}
}
function createSetupContext(instance) {
return {
attrs: instance.attrs
}
}
保留了两种 setup 返回值:
- 返回函数:直接把它作为组件的
render - 返回对象:保存到
setupState,并使用proxyRefs自动解包ref
最大的变化是:
instance.proxy = new Proxy(instance.ctx, publicInstanceProxyHandlers)
组件的 render 后续会使用这个代理对象作为 this。
实现代理拦截
- 读取属性时,依次查找
setupState、props和公共属性 - 修改属性时,将修改转发给
setupState
// packages/runtime-core/src/componentPublicInstance.ts
import { hasOwn } from '@vue/shared'
const publicPropertiesMap = {
$attrs: instance => instance.attrs,
$slots: instance => instance.slots,
$refs: instance => instance.refs
}
export const publicInstanceProxyHandlers = {
get(target, key) {
const { _: instance } = target
const { setupState, props } = instance
if (hasOwn(setupState, key)) {
return setupState[key]
}
if (hasOwn(props, key)) {
return props[key]
}
if (hasOwn(publicPropertiesMap, key)) {
const publicGetter = publicPropertiesMap[key]
return publicGetter(instance)
}
return instance[key]
},
set(target, key, value) {
const { _: instance } = target
const { setupState } = instance
if (hasOwn(setupState, key)) {
setupState[key] = value
}
return true
}
}
get 拦截
访问 this.count 时,代理会按照下面的顺序查找:
setupState -> props -> publicPropertiesMap
这样可以让组件在 render 中使用统一的写法:
this.count
this.msg
this.$attrs
this.$refs
其中,$attrs、$slots 和 $refs 并不是普通的组件状态,而是通过 publicPropertiesMap 映射到组件实例上的公共属性。
使用组件代理执行 render
修改为指向组件代理:
const subTree = instance.render.call(instance.proxy)
在 mountComponent 中,首次渲染和后续更新都需要使用 instance.proxy:
function mountComponent(vnode, container, anchor) {
const instance = createComponentInstance(vnode)
setupComponent(instance)
const componentUpdateFn = () => {
if (!instance.isMounted) {
const subTree = instance.render.call(instance.proxy)
patch(null, subTree, container, anchor)
instance.subTree = subTree
instance.isMounted = true
}
else {
const prevTree = instance.subTree
const subTree = instance.render.call(instance.proxy)
patch(prevTree, subTree, container, anchor)
instance.subTree = subTree
}
}
const effect = new ReactiveEffect(componentUpdateFn)
effect.run()
}
这样,render 中的 this 就不再只指向 setupState,而是指向组件公共实例代理。
小结
- 组件实例通过
ctx保存自身引用,通过proxy暴露公共访问入口 setupStatefulComponent创建组件代理,并处理setup的返回值get拦截器依次查找setupState、props和公共属性$attrs、$slots和$refs通过publicPropertiesMap暴露set拦截器将状态修改转发给setupState- 组件
render使用instance.proxy作为this