Vue3 的响应式系统源码级原始值响应式包装与类型转换原理
字数 1207 2025-11-23 01:20:05

Vue3 的响应式系统源码级原始值响应式包装与类型转换原理

1. 问题背景

Vue3 的响应式系统基于 Proxy 实现,但 Proxy 只能代理对象,无法直接代理原始值(如字符串、数字等)。而实际开发中,原始值也需要响应式能力。例如:

const count = ref(0); // 原始值 0 被包装为响应式

问题:Vue3 如何实现原始值的响应式?如何保证原始值在模板和响应式 API 中能正确工作?


2. 核心思路:Ref 的包装机制

Vue3 通过 ref 函数将原始值包装为一个带有 value 属性的响应式对象。核心步骤:

  1. 创建 Ref 对象:将原始值包装为 { value: ... } 的结构。
  2. 响应式化:对 value 属性进行响应式处理(通过 reactive 或直接劫持)。
  3. 类型转换:在模板和响应式 API 中自动脱 ref(无需手动调用 .value)。

3. 源码级实现解析

3.1 Ref 构造函数

源码位置:packages/reactivity/src/ref.ts

class RefImpl<T> {
  private _value: T
  private _rawValue: T // 存储原始值,用于对比
  public readonly __v_isRef = true // 标识为 ref

  constructor(value: T, public readonly _shallow = false) {
    this._rawValue = value
    // 如果是浅层 ref,直接存储值;否则用 reactive 转换
    this._value = _shallow ? value : toReactive(value)
  }

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

  set value(newVal) {
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : toReactive(newVal)
      trigger(this, TriggerOpTypes.SET, 'value', newVal) // 触发更新
    }
  }
}

关键点

  • toReactive(value) 会判断值类型:如果是对象,调用 reactive;如果是原始值,直接返回。
  • hasChanged 通过 Object.is 对比新旧值,避免不必要的更新。

3.2 自动类型转换(脱 Ref)

在模板和响应式 API 中,Vue3 会自动脱 ref(无需写 .value)。实现原理:

  1. 模板中的脱 Ref

    • 编译阶段,模板中的 ref 变量会被自动解包。例如:
      <div>{{ count }}</div> <!-- 编译为 count.value -->
      
    • 源码位置:packages/compiler-core/src/transforms/transformElement.ts 中的 processExpression 逻辑。
  2. 响应式 API 中的脱 Ref

    • reactive 包裹 ref 时,访问 ref 属性会自动解包:
      const count = ref(0)
      const obj = reactive({ count })
      console.log(obj.count) // 直接输出 0,而非 Ref 对象
      
    • 源码实现:packages/reactivity/src/reactive.ts 中的 get 陷阱:
      if (isRef(res)) {
        return res.value // 自动解包
      }
      

4. 特殊场景处理

4.1 嵌套 Ref 的处理

ref 包裹的值已经是 ref,则直接返回内部 ref,避免多层包装:

const count = ref(0)
const doubleCount = ref(count) // doubleCount 与 count 是同一个 ref

源码通过 isRef 检查实现:

export function ref(value?: unknown) {
  if (isRef(value)) return value // 直接返回已存在的 ref
  return new RefImpl(value)
}

4.2 响应式对象赋给 Ref

refvalue 被赋值为响应式对象时,会直接存储其原始值(而非响应式代理):

const obj = reactive({ foo: 1 })
const objRef = ref(obj) 
console.log(objRef.value === obj) // true,因为存储的是原始对象

源码通过 toReactive 中的 toRaw 转换实现:

const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

5. 总结

  • 原始值响应式:通过 ref 包装为 { value: ... } 对象,并对 value 属性劫持。
  • 自动脱 Ref:模板编译和 reactive 的 getter 中自动解包 ref.value
  • 性能优化:通过 hasChanged 避免重复触发更新,嵌套 ref 直接返回已有引用。
Vue3 的响应式系统源码级原始值响应式包装与类型转换原理 1. 问题背景 Vue3 的响应式系统基于 Proxy 实现,但 Proxy 只能代理对象,无法直接代理原始值(如字符串、数字等)。而实际开发中,原始值也需要响应式能力。例如: 问题 :Vue3 如何实现原始值的响应式?如何保证原始值在模板和响应式 API 中能正确工作? 2. 核心思路:Ref 的包装机制 Vue3 通过 ref 函数将原始值包装为一个带有 value 属性的响应式对象。核心步骤: 创建 Ref 对象 :将原始值包装为 { value: ... } 的结构。 响应式化 :对 value 属性进行响应式处理(通过 reactive 或直接劫持)。 类型转换 :在模板和响应式 API 中自动脱 ref(无需手动调用 .value )。 3. 源码级实现解析 3.1 Ref 构造函数 源码位置: packages/reactivity/src/ref.ts 关键点 : toReactive(value) 会判断值类型:如果是对象,调用 reactive ;如果是原始值,直接返回。 hasChanged 通过 Object.is 对比新旧值,避免不必要的更新。 3.2 自动类型转换(脱 Ref) 在模板和响应式 API 中,Vue3 会自动脱 ref(无需写 .value )。实现原理: 模板中的脱 Ref : 编译阶段,模板中的 ref 变量会被自动解包。例如: 源码位置: packages/compiler-core/src/transforms/transformElement.ts 中的 processExpression 逻辑。 响应式 API 中的脱 Ref : reactive 包裹 ref 时,访问 ref 属性会自动解包: 源码实现: packages/reactivity/src/reactive.ts 中的 get 陷阱: 4. 特殊场景处理 4.1 嵌套 Ref 的处理 若 ref 包裹的值已经是 ref ,则直接返回内部 ref ,避免多层包装: 源码通过 isRef 检查实现: 4.2 响应式对象赋给 Ref 当 ref 的 value 被赋值为响应式对象时,会直接存储其原始值(而非响应式代理): 源码通过 toReactive 中的 toRaw 转换实现: 5. 总结 原始值响应式 :通过 ref 包装为 { value: ... } 对象,并对 value 属性劫持。 自动脱 Ref :模板编译和 reactive 的 getter 中自动解包 ref.value 。 性能优化 :通过 hasChanged 避免重复触发更新,嵌套 ref 直接返回已有引用。