Skip to content

在 Vue 3 的 setup 函数中,开发者可以直接返回 ref 对象,而无需手动解包(即无需返回 { count: count.value })。例如:

text
setup() {
  const count = ref(0);
  return { count }; // 模板中 {{ count }} 直接显示 0
}

这种“魔法”背后的核心机制是 proxyRefs。它并非一个公开的 API,而是 Vue 内部用于处理 setup 返回值的关键技术。本节将深入剖析 proxyRefs 的实现原理,揭示 Vue 如何在返回 { count } 时自动处理 .value

一、问题的根源:ref 解包的上下文差异

如前所述,ref 在模板中会自动解包,但在 JavaScript 中必须通过 .value 访问。setup 函数返回的对象会同时被用于:

  1. 模板渲染:需要自动解包以简化模板语法。
  2. JavaScript 逻辑:可能在 setup 内部或其他地方被访问。

如果直接返回原始对象:

js
return { count: ref(0) };

setup 内部访问 count 时仍需 count.value,这与模板中的行为不一致。proxyRefs 的目标就是解决这一不一致性,使 ref 在返回对象中“表现得像普通值”。

二、proxyRefs 的实现原理

proxyRefs 的核心思想是:setup 返回的对象创建一个 Proxy,在读取和设置属性时自动对 ref 进行解包和打包

Vue 源码中的 proxyRefs 实现如下:

ts
export function proxyRefs(objectWithRefs) {
  return new Proxy(objectWithRefs, {
    get(target, key, receiver) {
      // 读取属性时,如果是 ref,则自动解包
      const value = Reflect.get(target, key, receiver);
      return isRef(value) ? value.value : value;
    },
    set(target, key, value, receiver) {
      // 设置属性时,如果是 ref,直接替换
      // 如果是普通值,但目标是 ref,则设置 ref.value
      const oldValue = target[key];
      if (isRef(oldValue) && !isRef(value)) {
        // 目标是 ref,但新值是普通值,应设置 ref.value
        oldValue.value = value;
        return true;
      } else {
        // 否则,直接设置属性(替换 ref 或设置普通值)
        return Reflect.set(target, key, value, receiver);
      }
    }
  });
}

详细解析:

  1. get 拦截器

    • 当访问 returnedObject.count 时,触发 get
    • 获取原始值 value = target['count']
    • 检查 value 是否为 ref(通过 isRef)。
    • 如果是 ref,返回 value.value(自动解包)。
    • 如果不是 ref,直接返回 value
    • 效果returnedObject.count 直接返回 0,而非 ref 对象。
  2. set 拦截器

    • 当设置 returnedObject.count = 1 时,触发 set
    • 获取旧值 oldValue = target['count']
    • 判断:
      • 如果 oldValueref,而新值 value 是普通值,则执行 oldValue.value = value(更新 ref 的内部值)。
      • 否则,直接通过 Reflect.set 替换属性(例如,用一个新 ref 或普通对象替换旧的)。
    • 效果returnedObject.count = 1 会正确更新 count ref 的值,而非替换整个 ref 对象。

三、setup 返回值的处理流程

setup 函数执行完毕并返回一个对象时,Vue 内部会对其进行处理:

ts
function setupStatefulComponent(instance) {
  const { setup } = Component;
  const setupResult = setup(props, setupContext);

  if (isFunction(setupResult)) {
    // 处理 setup 返回渲染函数的情况
  } else if (isObject(setupResult)) {
    // 对 setup 返回的对象应用 proxyRefs
    instance.setupState = proxyRefs(setupResult);
  }

  // ... 其他初始化逻辑
}

instance.setupState 是经过 proxyRefs 处理的代理对象。这个对象随后被:

  • 暴露给模板:模板编译器在生成渲染函数时,会访问 setupState 的属性。由于 get 拦截器的存在,ref 被自动解包。
  • 暴露给其他选项:在 computedwatch 等选项中,可以通过 this 访问 setup 返回的属性,同样享受自动解包的便利。

四、行为示例

js
setup() {
  const count = ref(0);
  const name = ref('Vue');
  const normalObj = { age: 25 };

  return { count, name, normalObj };
}

经过 proxyRefs 处理后:

操作行为
this.count (JS)返回 0(自动解包)
this.count = 1 (JS)执行 count.value = 1(自动打包)
(模板)显示 0(已解包)
this.normalObj返回 { age: 25 }(普通对象,无变化)
this.normalObj = { age: 30 }直接替换属性

五、与 toRefs 的对比

toRefs 也是一个将 reactive 对象的属性转换为 ref 的工具:

js
const state = reactive({ count: 0 });
const { count } = toRefs(state);

两者的关键区别:

特性proxyRefstoRefs
用途处理 setup 返回值解构 reactive 对象
返回值一个 Proxy 对象一个包含 ref 的普通对象
解包读取时自动解包读取时仍需 .value
设置设置值时自动处理 ref设置值必须通过 .value
响应性保持原 ref 的响应性保持与原 reactive 对象的连接

proxyRefs 更像是一个“透明代理”,让 ref 在对象中表现得像普通值;而 toRefs 是一个“转换工具”,显式地创建 ref

六、设计优势

proxyRefs 的设计带来了显著的开发体验提升:

  1. 一致性setup 返回的 ref 在模板和 JavaScript 中都表现为解包后的值,消除了上下文差异。
  2. 简洁性:开发者无需在 return 语句中手动解包 ref,代码更简洁。
  3. 透明性:对 ref 的操作被自动代理,开发者可以像操作普通对象一样操作返回值。
  4. 性能Proxy 的拦截开销极小,且仅在访问属性时发生,对性能影响可忽略。

七、潜在陷阱与注意事项

尽管 proxyRefs 非常便利,但仍需注意:

  1. 仅限 setup 返回值proxyRefs 是 Vue 内部机制,不应用于其他场景。在普通代码中,仍需手动处理 .value
  2. 动态属性访问
    js
    const key = 'count';
    this[key] = 1; // 仍然通过 proxyRefs 的 set 拦截器,正常工作
    动态访问也受 Proxy 拦截,因此仍能正确处理 ref
  3. 与 reactive 混合使用
    js
    const state = reactive({ count: 0 });
    return { ...state, count: ref(1) };
    这种混合通常不推荐,容易造成状态管理混乱。应明确区分 refreactive 的使用场景。

八、总结

proxyRefs 是 Vue 3 实现 setup 返回值自动解包的核心机制。它通过 Proxy 拦截 getset 操作:

  • get:自动解包 ref,返回 value.value
  • set:智能判断,若目标是 ref 则更新 value,否则直接替换。

这一机制使得 setup 返回的 ref 在模板和逻辑代码中都能以解包后的形式使用,极大地简化了组合式 API 的使用,是 Vue 3 响应式系统中一个精巧而强大的设计。理解 proxyRefs,有助于开发者更深入地掌握 Vue 3 的内部工作原理。