Vue3 的响应式系统源码级原始值类型自动装箱与拆箱(ref 的 .value 访问机制)实现原理
字数 1551 2025-12-11 19:51:53

Vue3 的响应式系统源码级原始值类型自动装箱与拆箱(ref 的 .value 访问机制)实现原理


1. 问题描述

在 Vue3 的响应式系统中,ref 不仅可以处理对象,还可以处理原始值(如数字、字符串、布尔值)。原始值在 JavaScript 中是不可变的,无法像对象那样被 Proxy 拦截。Vue3 如何实现原始值的响应式?为什么通过 ref 创建的响应式原始值需要通过 .value 访问?其内部的自动“装箱”和“拆箱”机制是如何实现的?


2. 核心概念:原始值的响应式困境

  • 原始值numberstringbooleannullundefinedsymbolbigint
  • 问题:Proxy 只能代理对象,无法直接代理原始值。若直接将原始值传给 reactive(),会直接返回原值,无法追踪变化。

3. 解决思路:对象包装(装箱)

Vue3 的解决方案是:将原始值包装成一个具有 value 属性的普通对象,然后对这个对象进行响应式处理。这个过程称为“装箱”。

// 伪代码示意
function ref(rawValue) {
  // 装箱:将原始值包装成 { value: rawValue }
  const refObject = {
    value: rawValue
  }
  // 对这个对象进行响应式处理
  return reactive(refObject)
}

这样,对 .value 的读写就能被 Proxy 拦截,从而实现依赖收集和触发更新。


4. 源码级实现解析

我们深入到 packages/reactivity/src/ref.ts 中看 ref 的实现。

步骤 1:创建 Ref 对象
class RefImpl<T> {
  private _value: T
  public readonly __v_isRef = true  // 标识这是一个 ref

  constructor(private _rawValue: T, public readonly __v_isShallow?: boolean) {
    // 如果是浅 ref,不进行深度响应式转换;否则,如果是对象,转为 reactive
    this._value = __v_isShallow ? _rawValue : convert(_rawValue)
  }

  get value() {
    // 依赖收集
    track(this, TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newVal) {
    // 使用 hasChanged 判断值是否真的变化(处理 NaN 等情况)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      // 转换新值:如果是对象,转为 reactive
      this._value = this.__v_isShallow ? newVal : convert(newVal)
      // 触发更新
      trigger(this, TriggerOpTypes.SET, 'value', newVal)
    }
  }
}
步骤 2:convert 函数处理对象转换
const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val
  • 如果 ref 接收的是对象,其 .value 会被 reactive() 包裹,成为深层响应式。
  • 如果接收的是原始值,convert 直接返回原始值,但此时原始值已被包装在 RefImpl 实例的 _value 中。
步骤 3:ref 工厂函数
function ref(value?: unknown) {
  return createRef(value, false)
}

function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

5. 自动拆箱(Unwrap)机制

在模板和 reactive 中,我们无需写 .value 即可访问 ref 的值,这是如何实现的?

5.1 模板中的自动拆箱

在编译阶段,模板中的 ref 会被自动加上 .value

<template>
  <div>{{ count }}</div>  <!-- 编译为 count.value -->
</template>
5.2 reactive 中的自动拆箱

reactive 对象中访问 ref 属性时,也会自动拆箱。这是通过 Proxy 的 get 拦截器实现的。

packages/reactivity/src/baseHandlers.tscreateGetter 中:

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    // ... 其他逻辑

    const res = Reflect.get(target, key, receiver)

    // 关键:如果获取的值是一个 ref,则返回其 .value(自动拆箱)
    if (isRef(res)) {
      // 如果是数组或原始值,直接拆箱;如果是对象,且不是只读,会进行依赖收集
      return res.value
    }

    // 如果是对象,且不是浅层,转为响应式
    if (isObject(res)) {
      return reactive(res)
    }

    return res
  }
}

注意:自动拆箱只发生在嵌套于 reactive 对象中,或模板编译阶段。在 JavaScript 中直接使用 ref 时,仍需手动 .value


6. 特殊 API:toRef 与 toRefs

这两个工具函数也与自动拆箱相关,常用于保持响应式链接。

6.1 toRef

将响应式对象的某个属性转换为一个 ref,保持与源属性的同步:

function toRef(object: any, key: string) {
  return new ObjectRefImpl(object, key)
}

class ObjectRefImpl {
  constructor(private readonly _object, private readonly _key) {}

  get value() {
    return this._object[this._key]  // 触发原对象的 getter
  }

  set value(newVal) {
    this._object[this._key] = newVal  // 触发原对象的 setter
  }
}

toRef 创建的 ref 是“链接式”的,修改其 .value 会直接影响原对象。

6.2 toRefs

将响应式对象的每个属性都转为 ref,常用于解构不丢失响应性:

function toRefs(object) {
  const ret: any = {}
  for (const key in object) {
    ret[key] = toRef(object, key)
  }
  return ret
}

7. 总结

  • 装箱ref 将原始值包装为 { value: rawValue } 对象,并用 RefImpl 实例代理,通过 getter/setter 拦截 .value 访问实现响应式。
  • 拆箱:在模板和 reactive 对象中,Vue 自动调用 .value 获取 ref 的实际值,提供语法便利。
  • 链接toRef/toRefs 创建的 ref 与源对象属性保持链接,修改会同步。

该机制使得 Vue3 能够统一处理原始值和对象的响应式,同时保持 API 的简洁性。

Vue3 的响应式系统源码级原始值类型自动装箱与拆箱(ref 的 .value 访问机制)实现原理 1. 问题描述 在 Vue3 的响应式系统中, ref 不仅可以处理对象,还可以处理原始值(如数字、字符串、布尔值)。原始值在 JavaScript 中是不可变的,无法像对象那样被 Proxy 拦截。Vue3 如何实现原始值的响应式?为什么通过 ref 创建的响应式原始值需要通过 .value 访问?其内部的自动“装箱”和“拆箱”机制是如何实现的? 2. 核心概念:原始值的响应式困境 原始值 : number 、 string 、 boolean 、 null 、 undefined 、 symbol 、 bigint 。 问题 :Proxy 只能代理对象,无法直接代理原始值。若直接将原始值传给 reactive() ,会直接返回原值,无法追踪变化。 3. 解决思路:对象包装(装箱) Vue3 的解决方案是: 将原始值包装成一个具有 value 属性的普通对象 ,然后对这个对象进行响应式处理。这个过程称为“装箱”。 这样,对 .value 的读写就能被 Proxy 拦截,从而实现依赖收集和触发更新。 4. 源码级实现解析 我们深入到 packages/reactivity/src/ref.ts 中看 ref 的实现。 步骤 1:创建 Ref 对象 步骤 2:convert 函数处理对象转换 如果 ref 接收的是对象,其 .value 会被 reactive() 包裹,成为深层响应式。 如果接收的是原始值, convert 直接返回原始值,但此时原始值已被包装在 RefImpl 实例的 _value 中。 步骤 3:ref 工厂函数 5. 自动拆箱(Unwrap)机制 在模板和 reactive 中,我们无需写 .value 即可访问 ref 的值,这是如何实现的? 5.1 模板中的自动拆箱 在编译阶段,模板中的 ref 会被自动加上 .value : 5.2 reactive 中的自动拆箱 在 reactive 对象中访问 ref 属性时,也会自动拆箱。这是通过 Proxy 的 get 拦截器实现的。 在 packages/reactivity/src/baseHandlers.ts 的 createGetter 中: 注意 :自动拆箱只发生在嵌套于 reactive 对象中,或模板编译阶段。在 JavaScript 中直接使用 ref 时,仍需手动 .value 。 6. 特殊 API:toRef 与 toRefs 这两个工具函数也与自动拆箱相关,常用于保持响应式链接。 6.1 toRef 将响应式对象的某个属性转换为一个 ref ,保持与源属性的同步: toRef 创建的 ref 是“链接式”的,修改其 .value 会直接影响原对象。 6.2 toRefs 将响应式对象的每个属性都转为 ref ,常用于解构不丢失响应性: 7. 总结 装箱 : ref 将原始值包装为 { value: rawValue } 对象,并用 RefImpl 实例代理,通过 getter/setter 拦截 .value 访问实现响应式。 拆箱 :在模板和 reactive 对象中,Vue 自动调用 .value 获取 ref 的实际值,提供语法便利。 链接 : toRef / toRefs 创建的 ref 与源对象属性保持链接,修改会同步。 该机制使得 Vue3 能够统一处理原始值和对象的响应式,同时保持 API 的简洁性。