2026.01.21

组件实例代理 -> PublicInstanceProxy 的设计与实现

在上一章中,我们实现了 Props 与 Attrs 的分离和初始化。然而,在 render 函数中使用 this.msg 时,会发现是无法访问的。

在上一章中,我们实现了 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 指向什么?如何让它能访问到 propssetupState

解决思路:

Vue 使用 Proxy 代理组件实例的上下文(ctx),让 this 通过代理层访问到 setupStateprops 等属性。

组件实例代理架构

实例结构设计

首先,我们需要在组件实例中添加 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
  }
}

访问优先级:

  1. setupState —— setup 返回的状态
  2. props —— 组件接收的 props
  3. 其他 —— 未定义(返回 undefined)

这样,在 render 函数中就可以通过 this.msgthis.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 函数中的 this
  • instance.ctx 作为代理的目标对象

2. 属性访问拦截

  • 优先级:setupState > props > 公共属性 > 实例本身
  • 支持动态查找,无需手动维护属性列表

3. 公共属性支持

  • 使用 publicPropertiesMap 映射表管理公共属性
  • 支持 $attrs$slotsrefs 等 Vue 内置属性
  • 易于扩展新的公共属性

4. 属性赋值拦截

  • 只允许修改 setupState 中的属性
  • 保护 props 不被意外修改(单向数据流)
  • 支持动态添加新属性到 setupState

5. 代码重构

  • 将 setup 相关逻辑拆分到 setupStatefulComponent
  • 保持代码的单一职责原则
  • 提升代码的可维护性和可测试性

这套代理机制为组件提供了统一而灵活的属性访问接口,让开发者可以在 render 函数中通过 this 自然地访问组件的各种属性,是 Vue 3 Composition API 与 Options API 融合的重要基础设施。