普通视图

发现新文章,点击刷新页面。
昨天 — 2025年11月27日首页

Vue响应式原理(13)-ref实现原理解析

2025年11月27日 14:17

Vue 3 中 Ref 实现原理解析

在 Vue 3 中,ref 是组合式 API(Composition API)的核心。很多开发者虽然会用,但对其内部运作机制、refreactive 的关系、以及为什么在scrit中我们访问 ref 数据需要用 .value, 但是在模板里不需要 .value 往往一知半解。

本文将剥离复杂的边界情况,用最精简的代码还原 Vue 3 源码的核心逻辑,带你彻底搞懂这三个问题:

  1. ref 是如何实现的?
  2. toRefs 是如何解决解构丢失响应性问题的?
  3. 为什么在模板中不需要 .value

1. Ref 的原理解析

为什么需要 Ref?

在先前的部分中,我们对响应式数据的原理进行了介绍,我们通过 reactive 函数处理一个对象来使其转变为响应式数据,而对于 JavaScript 中的原始类型(String, Number, Boolean, ...)是值传递的。如果你把一个数字传给一个函数,函数无法追踪这个数字的变化。为了让原始值变成“响应式”,我们需要把它包裹在一个对象中(Wrapper Pattern),利用对象的 gettersetter 来拦截访问和修改。

核心实现:RefImpl

Vue 3 内部通过 RefImpl 类来实现 ref

// 伪代码:简化版的 RefImpl
class RefImpl {
  private _value: any;
  private _rawValue: any;
  public dep: Dep; // 依赖容器
  public __v_isRef = true; // 标记这是一个 ref 对象

  constructor(value) {
    this._rawValue = value;
    // 如果传入的是对象,则通过 reactive 转换,否则保持原值
    this._value = isObject(value) ? reactive(value) : value;
    this.dep = new Set(); // 假设这是依赖收集容器
  }

  get value() {
    // 1. 依赖收集 (Track)
    trackEffects(this.dep); 
    return this._value;
  }

  set value(newVal) {
    // 只有值发生改变时才触发
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal;
      // 如果新值是对象,同样需要转换
      this._value = isObject(newVal) ? reactive(newVal) : newVal;
      // 2. 派发更新 (Trigger)
      triggerEffects(this.dep); 
    }
  }
}

// 暴露出来的 ref 函数
function ref(value) {
  return new RefImpl(value);
}

关键点解析

  1. ref 本质上会返回一个类的实例对象,这个对象拥有 .value 的访问器属性。
  2. __v_isRef:RefImpl 类需要增加一个 __v_isRef 属性用于区别 “Ref对象”与“含有 value 属性的普通对象”。ref 的本质是一个拥有 .value 属性的对象,但并不是所有拥有 .value 的对象都是 ref。如果不增加这个标识位,很难区分下面二者的区别:
// 真正的 ref
const realRef = ref(1); 
// realRef 结构: { value: 1, dep: Set, __v_isRef: true, ... }

// 用户不小心定义的普通对象
const fakeRef = { value: 1 };
// fakeRef 结构: { value: 1 }

Vue 的模板系统或者 reactive 尝试“自动解包”(读取 .value)时,如果没有 __v_isRef,系统可能会错误地把用户定义的 fakeRef 也当作响应式对象处理,去尝试读取它的依赖(dep),这会导致报错或逻辑混乱。

  1. Getter/Setter
    • get value():当访问 .value 时,调用 track 收集当前副作用函数(Effect)。
    • set value():当修改 .value 时,比较新旧值,若变化则调用 trigger 通知视图更新。
  2. 兼容对象参数:如果 ref(obj) 接收的是一个对象,源码中会调用 reactive(obj) 将其转化为深层响应式对象。这就是为什么 ref 可以包裹对象,且对象内部属性变化也能触发更新。

2. toRefs 的原理解析

为什么需要 toRefs?

当我们对一个 reactive 对象进行解构时,会丢失响应性,因为解构出来的只是普通的变量。

const state = reactive({ count: 1 });
const { count } = state; // count 此时只是一个普通数字 1,与 state 断开联系了

toRefs 的作用就是把 reactive 对象的每一个属性都转换成一个 ref,但这个 ref 比较特殊,它链接到了源对象。

核心实现:ObjectRefImpl

toRefs 内部并不是创建标准的 RefImpl,而是创建了 ObjectRefImpl。它不存储值,只是作为源对象属性的“代理”。

class ObjectRefImpl {
  public __v_isRef = true; // 标记为 ref

  constructor(
    private readonly _object, // 源 reactive 对象
    private readonly _key     // 指定的 key
  ) {}

  get value() {
    // 访问时,直接读取源对象的属性
    // 因为 _object 是响应式的,所以这里会自动触发源对象的依赖收集
    return this._object[this._key];
  }

  set value(newVal) {
    // 修改时,直接修改源对象的属性
    // 这里会自动触发源对象的更新派发
    this._object[this._key] = newVal;
  }
}

// toRef 函数:针对单个属性
function toRef(object, key) {
  return new ObjectRefImpl(object, key);
}

// toRefs 函数:遍历对象所有属性
function toRefs(object) {
  const ret = Array.isArray(object) ? new Array(object.length) : {};
  
  for (const key in object) {
    // 为每个属性创建一个 ObjectRefImpl
    ret[key] = toRef(object, key);
  }
  
  return ret;
}

关键点解析

  1. ObjectRefImpl 自身没有任何 tracktrigger 的逻辑。它只是把操作转发给了源 reactive 对象。当我们获取到 toRef 函数返回的对象时,我们对其 .value 属性的读写实际上会转发到对 this._object[this._key] 的读写,自然就会触发其 tracktrigger 的逻辑。
  2. toRefs 返回的是一个普通对象,里面的值全是 ref。这个普通对象可以被解构,解构出来的变量依然是 ObjectRefImpl 实例,依然保持着对源对象的引用。

3. 模板自动解包 (Unwrapping) 原理解析

现象

setup 中我们需要用 count.value,但在 <template> 中我们直接写 {{ count }} 即可。这是 Vue 在编译和渲染阶段做了特殊处理。

核心实现:proxyRefs

首先需要介绍两个辅助函数:

// 如果是 ref 返回 .value,否则返回原值
function unref(ref) {
  return isRef(ref) ? ref.value : ref;
}
// 根据对象 __v_isRef 属性判断其是否是 ref 对象
export function isRef(r: any): r is Ref {
  return !!(r && r.__v_isRef === true);
}

unref 函数首先判断传入的是否是 ref 对象,如果是则返回 ref.value, 否则返回 ref 本身,这个函数正是模板自动解包原理的核心。

Vue 在完成对模板的解析之后,将 setup 的返回值传递给渲染函数之前,会通过 proxyRefs 函数对其进行一层代理,在代理中拦截了 get 和 set 操作,并通过 unref 函数

const shallowUnwrapHandlers = {
  get: (target, key, receiver) => {
    // 1. 获取真实的值
    const value = Reflect.get(target, key, receiver);
    // 2. 自动解包:如果是 ref 就返回 value.value,否则直接返回
    return unref(value);
  },
  
  set: (target, key, value, receiver) => {
    const oldValue = target[key];
    // 3. 特殊处理:如果旧值是 ref,但新值不是 ref
    // 意味着用户想给 ref 赋值:count.value = 1
    if (isRef(oldValue) && !isRef(value)) {
      oldValue.value = value;
      return true;
    } 
    // 其他情况直接替换
    return Reflect.set(target, key, value, receiver);
  }
};

// Vue 内部会在 setupState 上套这一层 Proxy
function proxyRefs(objectWithRefs) {
  return new Proxy(objectWithRefs, shallowUnwrapHandlers);
}

运行流程

  1. 建立代理:当 setup() 函数返回一个对象(包含 ref)时,Vue 内部调用 handleSetupResult,使用 proxyRefs 包装这个返回对象,生成 render context(渲染上下文)。
  2. 模板读取
    • 当模板渲染遇到 {{ count }} 时,实际上是去在这个 Proxy 对象上取 count
    • 触发 get 拦截:发现 count 是一个 ref,Proxy 自动帮你调用 .value 并返回结果。
  3. 模板赋值(例如 v-model):
    • 如果在模板中写 <input v-model="count">
    • 触发 set 拦截:Proxy 发现 count 原本是 ref,而输入的是普通值,它会将新值赋值给 count.value

总结

特性 核心实现类/函数 关键原理
ref RefImpl 利用 getter/setter 劫持 .value 属性,通过 track/trigger 管理依赖。若值为对象则借助 reactive
toRefs ObjectRefImpl 不存值,仅仅是对源 reactive 对象属性的代理访问。解决了解构导致的响应性丢失问题。
模板解包 proxyRefs 利用 Proxy 拦截 setup 返回对象的访问,遇到 ref 自动返回 .value,实现由模板到数据的无感读写。

通过阅读这部分源码,我们可以看到 Vue 3 在易用性(自动解包)和灵活性(ref/reactive 分离)之间做了非常精妙的设计。

❌
❌