组件实例代理 -> PublicInstanceProxy 的设计与实现
在上一章中,我们实现了 Props 与 Attrs 的分离和初始化。然而,在 render 函数中使用 this.msg 时,会发现是无法访问的。
这是因为组件实例(instance)与 render 函数中使用的 this 之间需要一个代理层来桥接。
本章将深入组件实例代理系统的设计与实现,理解 Vue 如何通过 Proxy 实现属性访问的优雅拦截。
问题场景
让我们先看一个典型的渲染函数访问属性场景:
<div id="app"></div>
<script type="module">
import { h, createApp, ref } from '../dist/vue.esm.js'
const Comp = {
props: {
msg: String
},
setup(props, { attrs }) {
console.log('props:', props) // { msg: 'msg' }
console.log('attrs:', attrs) // { count: 0, a: 1 }
const aa = ref('aa')
return { aa }
},
render() {
// 这里希望通过 this 访问 props 和 setup 返回的状态
return h('div', this.msg)
}
}
createApp(Comp, { msg: 'msg', count: 0, a: 1 }).mount('#app')
</script>
核心问题:
在 render 函数中,this 指向什么?如何让它能访问到 props 和 setupState?
解决思路:
Vue 使用 Proxy 代理组件实例的上下文(ctx),让 this 通过代理层访问到 setupState、props 等属性。
组件实例代理架构
实例结构设计
首先,我们需要在组件实例中添加 proxy 属性:
/**
* 创建组件实例
*/
export function createComponentInstance(vnode) {
const { type } = vnode
const instance = {
type,
vnode,
render: null, // 渲染函数
setupState: null, // setup 返回的状态
propsOptions: normalizePropsOptions(type.props), // 用户声明的组件 props
props: {}, // 实际接收的 props 值
attrs: {}, // 实际接收的 attrs 值
subTree: null, // 子树(render 的返回值)
isMounted: false, // 是否已挂载
proxy: null // 组件实例代理(render 中使用的 this)
}
instance.ctx = { _: instance } // 上下文对象,内部使用
return instance
}
核心设计:
instance.ctx:组件上下文对象,用于内部使用instance.proxy:代理对象,在render函数中作为this使用
重构 setupComponent
我们将 setup 相关的逻辑拆分到 setupStatefulComponent 函数中:
/**
* 初始化组件
*/
export function setupComponent(instance) {
initProps(instance)
setupStatefulComponent(instance)
}
/**
* 初始化有状态组件
*/
function setupStatefulComponent(instance) {
const { type } = instance
/**
* 创建代理对象,绑定到 instance.proxy
* render 函数中的 this 将指向这个代理对象
*/
instance.proxy = new Proxy(instance.ctx, publicInstanceProxyHandlers)
/**
* 创建 setup 的第二个参数 context
*/
const setupContext = createSetupContext(instance)
/**
* 执行 setup 函数
*/
if (isFunction(type.setup)) {
const setupResult = proxyRefs(type.setup(instance.props, setupContext))
instance.setupState = setupResult
}
/**
* 绑定 render 函数
*/
instance.render = type.render
}
架构优势:
- 职责分离:
setupComponent负责组件初始化流程,setupStatefulComponent负责有状态组件的具体实现 - 代理模式:通过 Proxy 拦截属性访问,实现灵活的属性查找逻辑
实现代理拦截器
基础属性访问拦截
/**
* 组件实例代理拦截器
* 实现 render 函数中 this 的属性访问拦截
*/
const publicInstanceProxyHandlers = {
get(target, key) {
const { _: instance } = target
const { setupState, props } = instance
/**
* 1. 优先从 setupState 中查找
*/
if (hasOwn(setupState, key)) {
return setupState[key]
}
/**
* 2. 其次从 props 中查找
*/
if (hasOwn(props, key)) {
return props[key]
}
/**
* 3. 未找到则返回 undefined
*/
return undefined
}
}
访问优先级:
setupState—— setup 返回的状态props—— 组件接收的 props- 其他 —— 未定义(返回 undefined)
这样,在 render 函数中就可以通过 this.msg 和 this.aa 访问 props 和 setup 返回的状态了。
公共属性访问:$attrs、$slots、$refs
Vue 提供了一些公共属性供组件实例访问,如 $attrs、$slots、$refs。我们需要在代理拦截器中支持这些属性。
扩展实例结构
export function createComponentInstance(vnode) {
const { type } = vnode
const instance = {
// ...
slots: {}, // 插槽数据
refs: {} // 引用数据
}
instance.ctx = { _: instance }
return instance
}
公共属性映射表
/**
* 公共属性映射表
* 将 $attrs、$slots、$refs 映射到实例对应的属性
*/
const publicPropertiesMap = {
$attrs: instance => instance.attrs,
$slots: instance => instance.slots,
$refs: instance => instance.refs
}
更新代理拦截器
const publicInstanceProxyHandlers = {
get(target, key) {
const { _: instance } = target
const { setupState, props } = instance
/**
* 1. 优先从 setupState 中查找
*/
if (hasOwn(setupState, key)) {
return setupState[key]
}
/**
* 2. 其次从 props 中查找
*/
if (hasOwn(props, key)) {
return props[key]
}
/**
* 3. 检查是否是公共属性
*/
if (hasOwn(publicPropertiesMap, key)) {
const publicGetter = publicPropertiesMap[key]
return publicGetter(instance)
}
/**
* 4. 未找到则返回实例本身
*/
return instance
}
}
- 使用映射表(
publicPropertiesMap)而非 if-else 判断,易于扩展 - 使用函数形式的映射,支持懒加载和动态计算
支持属性赋值
除了访问属性,我们还需要支持属性的赋值操作,比如在事件处理函数中修改状态:
<script type="module">
import { h, createApp, ref } from '../dist/vue.esm.js'
const Comp = {
props: {
msg: String
},
setup(props, { attrs }) {
return {}
},
render() {
return h('div', {
onClick: () => {
this.count++ // 尝试修改 attrs 中的 count
}
}, this.count)
}
}
createApp(Comp, { msg: 'msg', count: 0, a: 1 }).mount('#app')
</script>
要注意的是:
setupState中的属性可以修改props中的属性不允许直接修改(单向数据流)
实现 set 拦截
const publicInstanceProxyHandlers = {
get(target, key) {
// ... get 逻辑
},
/**
* 属性赋值拦截
*/
set(target, key, value) {
const { _: instance } = target
const { setupState, props } = instance
/**
* 只允许修改 setupState 中的属性
* props 不允许直接修改(单向数据流)
*/
if (hasOwn(setupState, key)) {
setupState[key] = value
return true
}
/**
* 尝试修改 props 会触发警告(生产环境)
*/
if (hasOwn(props, key)) {
console.warn(`Attempting to mutate prop "${key}". Props are readonly.`)
return false
}
/**
* 其他属性赋值,直接设置到 setupState
*/
setupState[key] = value
return true
}
}
设计原则:
- 单向数据流:props 是只读的,不允许子组件修改
- 灵活扩展:未声明的属性可以动态添加到
setupState - 安全性:避免意外的属性污染
总结
至此,我们完成了 Vue 组件实例代理系统的核心实现:
1. 代理架构设计
- 通过
Proxy拦截组件实例的属性访问 instance.proxy作为render函数中的thisinstance.ctx作为代理的目标对象
2. 属性访问拦截
- 优先级:
setupState>props> 公共属性 > 实例本身 - 支持动态查找,无需手动维护属性列表
3. 公共属性支持
- 使用
publicPropertiesMap映射表管理公共属性 - 支持
$attrs、$slots、refs等 Vue 内置属性 - 易于扩展新的公共属性
4. 属性赋值拦截
- 只允许修改
setupState中的属性 - 保护
props不被意外修改(单向数据流) - 支持动态添加新属性到
setupState
5. 代码重构
- 将 setup 相关逻辑拆分到
setupStatefulComponent - 保持代码的单一职责原则
- 提升代码的可维护性和可测试性
这套代理机制为组件提供了统一而灵活的属性访问接口,让开发者可以在 render 函数中通过 this 自然地访问组件的各种属性,是 Vue 3 Composition API 与 Options API 融合的重要基础设施。