通过组件实例代理统一访问 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:访问组件的 props
  • this.$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。

实现代理拦截

  1. 读取属性时,依次查找 setupState、props 和公共属性
  2. 修改属性时,将修改转发给 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,而是指向组件公共实例代理。

小结

  1. 组件实例通过 ctx 保存自身引用,通过 proxy 暴露公共访问入口
  2. setupStatefulComponent 创建组件代理,并处理 setup 的返回值
  3. get 拦截器依次查找 setupState、props 和公共属性
  4. $attrs、$slots 和 $refs 通过 publicPropertiesMap 暴露
  5. set 拦截器将状态修改转发给 setupState
  6. 组件 render 使用 instance.proxy 作为 this