Skip to content

ref 与 shallowRef:原始值是如何实现响应式的

在上一节中,我们了解到 reactive 函数使用 Proxy 来实现对象的响应式。但 Proxy 只能代理对象类型(object、array),不能代理 string、number、boolean 这样的原始值。

那么,Vue 3 是如何让我们手中的 count = 0 这种原始值也实现响应式的呢?

答案是:将原始值“包装”到一个对象中,并通过该对象的 .value 属性来访问和修改,从而实现拦截。

这个“包装对象”,就是 ref

RefImpl:ref 的内部实现

当你调用 ref(0) 时,Vue 在内部创建了一个 RefImpl 类的实例。这是一个专用于“包装”值的类:

typescript
// core/packages/reactivity/src/ref.ts

// 这是一个简化的 RefImpl 类
class RefImpl<T> {
  // 盒子里的“原始金币”
  private _rawValue: T
  // 盒子里的“响应式金币”
  // (如果 T 是对象,这里会是 T 的 reactive 代理)
  private _value: T

  // 每个盒子都自带一个“依赖收集器” (Dep)
  // 就像一个“订阅了此盒子”的列表
  public dep: Dep = new Dep()

  // 盒子的“身份证”
  public readonly [ReactiveFlags.IS_REF] = true

  constructor(value: T, isShallow: boolean) {
    this._rawValue = value
    
    // 关键!如果放进盒子的是个对象,
    // ref 会自动用 toReactive (即 reactive()) 把它变成响应式
    this._value = isShallow ? value : toReactive(value)
  }
}

RefImpl 的设计有两个关键点:

  1. 自带 dep:每个 ref 都是一个独立的“响应式单元”,它自己负责收集(track)和触发(trigger)依赖。
  2. 自动 reactiverefreactive 并不是孤立的。如果你把一个对象塞进 refref({ a: 1 }) 它会自动帮你用 reactive 代理这个对象。

.value:访问器:依赖收集与更新触发

RefImpl最核心的机制在于它的 .value 属性,它并不是一个普通属性,而是一个get / set访问器。

typescript
// RefImpl 类的 .value 访问器
class RefImpl<T> {
  // ... 
  
  // 当读取 .value 属性时 (get)
  get value() {
    // 1. 【依赖收集】
    //    调用 track(),将当前正在运行的 effect 注册到 dep
    this.dep.track() 
    
    // 2. 返回内部存储的值
    return this._value
  }

  // 当设置 .value 属性时 (set)
  set value(newValue) {
    // 1. 检查新旧值是否有变化
    if (hasChanged(newValue, this._rawValue)) {
      // 2. 更新内部的原始值和处理后的值
      this._rawValue = newValue
      this._value = toReactive(newValue) // 新值同样需要尝试转为 reactive
      
      // 3. 【派发更新】
      //    通知所有订阅了此 ref 的 effect 重新运行
      this.dep.trigger() 
    }
  }
}

ref 的全部实现原理就是:

  • 将一个值(原始值或对象)包装成 RefImpl 实例。
  • get value 访问器负责依赖收集 (track)。
  • set value 访问器负责派发更新 (trigger)。

shallowRef:浅层 ref

ref 会自动对传入的对象进行 reactive 深层代理。有时我们有一个大型对象,希望跳过这个深层代理,只在 .value 根引用被替换时才更新,这时就可以使用 shallowRef

答案是 shallowRef,一个“懒盒子”。

typescript
// ref 和 shallowRef 都由 createRef 工厂创建
function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue // 如果传入的已经是 ref,直接返回
  }
  // shallow 标志位会传递给 RefImpl 的构造函数
  return new RefImpl(rawValue, shallow)
}

export function ref(value?: unknown) {
  return createRef(value, false) // ref 传入 shallow: false
}
export function shallowRef(value?: unknown) {
  return createRef(value, true) // shallowRef 传入 shallow: true
}

shallowRef 的“懒”体现在两个地方:

  1. set value(newVal):在 RefImpl 构造函数和 set 中,shallowtrue 会阻止 toReactive(newValue) 的调用。盒子会原封不动地存入新值。
  2. 触发时机:它只在 .value 本身被替换时(state.value = newObj)才会触发。你修改 state.value.count = 2无效的,因为盒子里的对象不是响应式的,盒子本身也不知道它内部发生了变化。

何时使用? 当你有一个巨大的、不可变的数据结构,你只想在它整个被替换时才更新视图,shallowRef 是绝佳的性能优化选择。

toRefs:解决 reactive 解构丢失响应性的问题

ref 最重要的应用场景之一,是解决 reactive 最大的痛点:解构会丢失响应性

javascript
const state = reactive({ count: 0, name: 'Vue' })

// 致命操作:解构
// count 只是一个普通的数字 0,它和 state.count 已经没关系了
// 改变 count 不会触发任何更新
const { count, name } = state

为了解决这个问题,Vue 提供了 toRefs

javascript
// 正确的做法
const stateAsRefs = toRefs(state)
// stateAsRefs 是 { count: Ref<0>, name: Ref<'Vue'> }
const { count, name } = stateAsRefs

toRefs 是如何把 reactive 对象的属性,变成一个个 ref 的呢?它创建的 refRefImpl 可不一样,而是 ObjectRefImpl

typescript
// toRef (toRefs 是 toRef 的批量调用)
function propertyToRef(source: Record<string, any>, key: string) {
  return new ObjectRefImpl(source, key)
}

// ObjectRefImpl 的实现
class ObjectRefImpl<T extends object, K extends keyof T> {
  public readonly [ReactiveFlags.IS_REF] = true

  constructor(private readonly _object: T, private readonly _key: K) {}

  // get value:
  // 它的 get 访问器,会“委托”给原始 reactive 对象的属性读取
  get value() {
    return this._object[this._key]
  }

  // set value:
  // 它的 set 访问器,会“委托”给原始 reactive 对象的属性设置
  set value(newVal) {
    this._object[this._key] = newVal
  }
}

ObjectRefImpl 是一个天才设计:

  1. 它本身不存储任何值。
  2. 它是一个 ref(有 .value),所以你可以安全地解构和传递它。
  3. 它的 get/set 直接访问原始 reactive 对象的属性。
  4. 由于它访问了原始 reactive 对象,它会触发原始对象的代理(Proxy),从而自动完成了依赖收集和触发

toRefsreactive 对象的每个属性都创建了一个“属性引用”,确保解构后响应性不丢失。

自动解包 (proxyRefs):为什么模板里不用 .value

我们费尽心机地使用了 .value,可为什么在 <template> 里却可以直接写 ,而不是 count.value

因为 Vue 帮我们做了一层代理,proxyRefs

当我们的 setup() 函数返回一个 { count: ref(0) } 对象时,Vue 会立刻用 proxyRefs 把这个对象包起来。

typescript
// core/packages/reactivity/src/ref.ts

// proxyRefs 的处理器
const shallowUnwrapHandlers: ProxyHandler<any> = {
  get: (target, key, receiver) => {
    // 1. 正常读取属性值
    const value = Reflect.get(target, key, receiver)
    
    // 2. 【自动解包】
    //    如果读出的是一个 ref,则自动返回它的 .value
    return isRef(value) ? value.value : value
  },

  set: (target, key, value, receiver) => {
    const oldValue = target[key]
    
    // 3. 【智能写入】
    //    如果旧值是 ref,而新值不是 ref,
    //    则自动写入旧值的 .value 属性
    if (isRef(oldValue) && !isRef(value)) {
      oldValue.value = value
      return true
    } else {
      // 否则,正常替换
      return Reflect.set(target, key, value, receiver)
    }
  },
}

// setup() 返回的对象会被这样包装
proxy = new Proxy(objectWithRefs, shallowUnwrapHandlers)

proxyRefs 是一个 Proxy 代理。它在 get 时自动解包 .value,在 set 时智能写入 .value``。reactive 在设置属性时也会使用此逻辑,这就是 reactive({ a: ref(0) }) 也可以自动解包的原因。

总结

Vue 3 的 ref 系统是一套用于包装值的响应式机制:

  1. RefImpl:是 ref 的核心实现,它是一个包装类,通过 .valueget/set 访问器来调用 tracktrigger
  2. ref vs shallowRefref 会对对象值自动调用 reactiveshallowRef 则不会,它只对 .value 自身的替换操作做出响应。
  3. toRefs / ObjectRefImpl:用于解决 reactive 解构丢失响应性的问题。它创建的 ref 会将 get/set 操作委托回源 reactive 对象的 Proxy
  4. proxyRefs (自动解包):用于 setup 的返回值,它在 get 时自动返回 ref.value,让我们在模板中可以省略 .value

微信公众号二维码

Last updated: