Vue3中 toRef 和 ref 的理解
写作背景
toRef 和 ref 是两个Vue3中常用的API,开发者可以在代码中去灵活调用这两个 Vue3 的 API 方法来达到自己想要的效果。
前端时间学习 Vue3 源码时看过一些文章,那时候对这两个 API 有疑问,于是网上搜索过很多文章。
- Vue3 toRef 和 toRefs 函数
- vue3 中 toRef 和 toRefs 的情况(系列九)
- vue3 中 toRef 理解
- ...
这些文章在百度、谷歌都能搜到,并被大量阅读
最近由于要围绕 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响应式对象,做了以下几个步骤:####
-
调用 ref 函数时,会调用一个** createRef** 函数,如果对象是一个已经被 ref 函数封装过的对象,则直接返回。
-
若不是 ref 对象,则构建 RefImpl 对象。可以看见 通过 RefImpl 类构建出来的实例,在访问、更新其 value 的时候,分别进行了依赖收集与派发更新。
-
构建实例时存放了一份 rawValue(原始数据) 和value(通过 toReactive 构建的响应式数据)
-
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 最后得到的是一个数值 0,test2.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 对象,value 的 get 方法。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 只是构建了一个与原来响应式对象的一个连接(引用),依赖收集与派发更新最终还是原对象的响应式。
而且 toRef 和 ref 两个方法构建出来的对象,并非属于同一个类或是又继承关系。两个方法最后返回的实例,一个是 Class ObjectRefImpl,另一个是 Class RefImpl,两者在属性上只有一个 __v_isRef 相同