Vue3中toRef 和 ref的理解2.0

565 阅读7分钟

Vue3中 toRefref 的理解

写作背景

toRefref 是两个Vue3中常用的API,开发者可以在代码中去灵活调用这两个 Vue3 的 API 方法来达到自己想要的效果。

前端时间学习 Vue3 源码时看过一些文章,那时候对这两个 API 有疑问,于是网上搜索过很多文章。

  1. Vue3 toRef 和 toRefs 函数
  2. vue3 中 toRef 和 toRefs 的情况(系列九)
  3. vue3 中 toRef 理解
  4. ...

这些文章在百度、谷歌都能搜到,并被大量阅读

最近由于要围绕 ref、toRef、toRefs 来进行分享。

笔者认为这些文章有一个不太对的观点:

普遍观点是:toRef 方法与 ref 方法一样都是用来创建响应性数据的。

如果你的观点也是这样,那可以阅读完全文,读完后如果还认为这个观点是对的。那么可以指出我文章中的错误的地方,本人持续保持虚心学习的态度的,本文目的最终还是想透过这个问题来学习Vue3源码设计思路。


理解ref

官方说明

接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value 在《Vue 设计与实现》的书中,明确了 ref 是原始值数据响应式方案;

官方例子:

const test = ref(0);
console.log(test.value); // 0
test.value++;
console.log(test.value); // 1

调用确实很简单,接下来我们再带着问题来看看源码。

// packages/reactivity/src/ref.ts

.... //省略ref函数ts重写
export function ref(value?: unknown) {
  return createRef(value, false) // 第二个参数为false,为true的话则为shallowRef,具体可以看shallowRef的实现
}
function createRef(rawValue: unknown, shallow: boolean) {
  // 如果是ref(对象上有__v_isRef属性 === true)了,直接返回
  if (isRef(rawValue)) {
    return rawValue
  }
  //否则 构建RefImpl对象
  return new RefImpl(rawValue, shallow)
}

// 判断 ref 是否 已经是一个ref对象,如果是,直接返回true,会在createRef过程中写入。
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref {
  return !!(r && r.__v_isRef === true)
}

class RefImpl<T> {
  private _value: T
  private _rawValue: T
  public dep?: Dep = undefined
  public readonly __v_isRef = true
  /**
   * 构造函数
   * @param value 传入的原始值
   * @param __v_isShallow 判断是否首层ref
   * public readonly __v_isShallow: boolean 这种写法呢,就是直接赋值给对象,并定义为readonly
   */
  constructor(value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = __v_isShallow ? value : toRaw(value)  // 原始值
    this._value = __v_isShallow ? value : toReactive(value) // 创建 reactive对象
  }
  get value() {
    // 依赖收集
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    // 判断是否首层,判断新值是否首层,是否只读,是的话直接赋值, false的话 toRaw
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    // toRaw(1) 返回值 1
    newVal = useDirectValue ? newVal : toRaw(newVal)
    // 判断赋值的时候 是否是可以直接赋值。
    // 如 const a = ref(0)
    // effect(()=>{ console.log( a.value)})
    //a.value = 1
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      // 派发更新
      triggerRefValue(this, newVal)
    }
  }
}

通过这部分源码,能够清楚看到 构建一个ref响应式对象,做了以下几个步骤:####

  1. 调用 ref 函数时,会调用一个** createRef** 函数,如果对象是一个已经被 ref 函数封装过的对象,则直接返回。

  2. 若不是 ref 对象,则构建 RefImpl 对象。可以看见 通过 RefImpl 类构建出来的实例,在访问、更新其 value 的时候,分别进行了依赖收集与派发更新。

  3. 构建实例时存放了一份 rawValue(原始数据) 和value(通过 toReactive 构建的响应式数据)

  4. toReactive 是当传入类型为对象时才会执行 reactive 对象,否则直接返回原值

export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value
const test1 = ref({ x: 1, b: 2 });
const test2 = ref(0);

根据上面的例子:

  • test1.value 值为 一个经过 reactive 过的 {x:1,b:2},因为 test1.value 中的值进行过 toReactive 操作,当传入值是一个对象时,如上代码,会执行执行一次 reactive 方法,访问或者更新 test1.value.x 时,是能够正常进行响应式的。

  • test2 最后得到的是一个数值 0test2.value++ 时 触发了 trackRefValue,更新时会触发 triggerRefValue

上面就是 ref 构建一个响应式数据的过程,大家可以落实到看源码。


下面讲讲 toRef 和 toRefs

回到文章一开始提出的错误观点:toRef 方法与 ref 方法一样都是用来创建响应性数据的

那如果 toRef 的入参是一个普通对象,那创建出来的还是响应式对象吗?

如:

const obj = { x: 1 };
let state = toRef(obj, x);

// 请问state是响应式对象吗?

明显不是。

下面,我们可以看看 toRef 的源码:

可以看到:
toRef 会先判断对象是否有v_isRef (通过 ref,和 toRef 构建的对象 都会有 v_isRef 属性,且为 true),然后实例化一个 ObjectRefImpl 的对象

但两个真的是同一个东西吗?

  • toRef 创建的是 ObjectRefImpl 对象,
  • ref 创建的 是 RefImpl 对象,
  • 虽然在命名上好像存在继承关系,但是实际上 两个对象 除了有一个 readonly 的 __v_isRef 属性外,毫无关联
  • 可以看到 ObjectRefImpl,是没有任何派发更新,依赖收集的动作的。
export function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K,
  defaultValue?: T[K]
): ToRef<T[K]> {
  const val = object[key]
  return isRef(val)
    ? val
    : (new ObjectRefImpl(object, key, defaultValue) as any)
}

class ObjectRefImpl<T extends object, K extends keyof T> {
  public readonly __v_isRef = true

  constructor(
    // 参数并赋值
    private readonly _object: T,
    private readonly _key: K,
    private readonly _defaultValue?: T[K]
  ) {}

  get value() {
    const val = this._object[this._key]
    return val === undefined ? (this._defaultValue as T[K]) : val
  }

  set value(newVal) {
    this._object[this._key] = newVal
  }
}

那么所谓的 toRef 创建一个响应式对象 是什么东西? 其实这里我可以指出一下我的观点:

ref 是构建了一个响应式数据,toRef仅仅是对响应式数据的一个连接(引用)

举个例子吧:

//example1
const a = reactive({ x: 1 });
const b = toRef(a, "x");
effect(() => {
	console.log(b.value);
});
setTimeOut(() => {
	b.value = 2;
}, 2000);
return {
	b,
};
//example2
const a = { x: 1 };
const b = toRef(a, "x");
effect(() => {
	console.log(b.value);
});
setTimeOut(() => {
	b.value = 2;
}, 2000);
return {
	b,
};

哪段 2s 后会有打印?

答案是example1

我们看看 example1 到底触发了些什么?

  • step1:reactive 创建响应式对象, reactive 通过 proxy 实现,在 baseHandler 里有设置依赖收集与派发更新。
  • step2: 调用 toRef,传入对象 a,属性'x',按照上述代码,会调用 ObjectRefImpl 创建对象,响应式对象就存放到了 _object 属性当中,_key 记录传入的属性'x'
  • step3: effect 函数执行,里面有读取到 b.value,b.value 会触发 ObjectRefImpl 对象,valueget 方法。const val = this._object[this._key] 这行,已经对 原来的响应式对象 object 上的进行读操作(此时依赖收集)。
  • step4: 2s 后 执行定时器里的 b.value = 2 次数出发了 b.value 的 set 方法。 this._object[this._key] = newVal 直接更新响应式对象的

官方的说明

toRef 的官方说明:
基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。

官方的说明中,指出了toRef 函数能够创建一个响应式对象,是基于响应式对象的一个属性,并不是基于一个普通对象属性。

而且 这里所说的 ref(toRef 创建)与通过 ref 创建的响应式数据完全不是同一个东西。

toRef 创建的是依赖原来响应式对象的响应式。 ref 创建的响应式数据依赖的是自身的响应式。


最后总结

toRef 方法与 ref 方法一样都是用来创建响应性数据的

这个观点,并非完全正确,通过 toRef 构建的话,要求传入的对象本身就需要是响应式数据,传入普通对象的话,是不能构建出响应式数据的。 toRef 只是构建了一个与原来响应式对象的一个连接(引用),依赖收集与派发更新最终还是原对象的响应式。

而且 toRefref 两个方法构建出来的对象,并非属于同一个类或是又继承关系。两个方法最后返回的实例,一个是 Class ObjectRefImpl,另一个是 Class RefImpl,两者在属性上只有一个 __v_isRef 相同