实现ref
mini-vue3的同步代码实现点击这里
mini-vue3的所有文章点击这里
实现ref
在vue3中我们想要一个数据变成响应式数据,可以通过reactive
也可以通过ref
。但是因为reactive
本质上是使用了proxy
实现的响应式,所以reactive
是不能将基本数据类型的数据作为参数使其变成响应式。所以当我们想要将基本数据类型变成响应式是我们可以使用ref。ref
可以基本数据类型和对象都变成响应式的。
实现通过.value获取值
// ref.spec.ts
import { ref } from './ref'
describe('ref', () => {
it('happy path', () =>{
const a = ref(1)
// 获取a的值需要通过 .value的形式获取
expect(a.value).toBe(1)
})
})
// ref.ts
class RefImplement {
private _value
constructor(value) {
this._value = value
}
get value() {
return this._value
}
set value(newValue) {
this._value = newValue
}
}
export function ref(value) {
return new RefImplement(value)
}
想要通过.value
获取值,我们可以创建一个RefImplement
的类,然后对value
编写相应的getter
和setter
操作,这样就可以实现通过.value
获取值。
实现ref的响应式
在前面文章实现的effect
函数中,如果传入的fn
参数中有访问了ref
对象的.value
,那么当这个ref
对象的.value
发生变化时,fn
应该被重新执行。同时当赋值相同的value时,不会触发trigger。
// ref.spec.ts
it("should be reactive", () => {
const a = ref(1);
let dummy;
let calls = 0;
effect(() => {
calls++;
dummy = a.value;
});
expect(calls).toBe(1);
expect(dummy).toBe(1);
// 修改值
a.value = 2;
// 重新执行函数
expect(calls).toBe(2);
expect(dummy).toBe(2);
// 相同value不会触发trigger
a.value = 2;
expect(calls).toBe(2);
expect(dummy).toBe(2);
});
想要实现ref
的响应式,那么就需要在get value
的时候收集依赖,在set value
的时候触发依赖。因为收集依赖和触发依赖的操作,在effct.ts
文件中有实现过,但是这里并不能直接使用effect.ts
的依赖收集和触发依赖函数。所以需要对之前的track
和trigger
函数进行修改。
// effect.ts
...
export function isTracking() {
return shouldTrack && activeEffect !== undefined
}
function track(target, key) {
if (!isTracking()) return
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
// 将后续的代码封装成一个函数
trackEffect(dep);
}
export function trackEffect(dep) {
if (activeEffect) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
export function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (!dep) return;
triggerEffect(dep);
}
export function triggerEffect(dep) {
for (const effect of dep) {
// 在后面的实现中,被收集的依赖会有run方法,在该方法中会执行真正需要执行的函数
if (typeof effect.scheduler === "function") {
effect.scheduler();
} else {
effect.run();
}
}
}
在effect.ts
文件中,我们封装了isTracking
函数,该函数用来判断是否应该进行依赖收集。并在track
函数中调用trackEffect
函数,在trigger
函数中调用triggerEffect
函数。这样做的好处是,在实现ref
的依赖收集和触发依赖时,可以直接使用trackEffect
和triggerEffect
函数进行收集。
// ref.ts
import { isTracking, trackEffect, triggerEffect } from './effect'
class RefImplement {
private deps // 用于收集依赖
private _value
constructor(value) {
this._value = value
this.deps = new Set()
}
get value() {
//判断是否需要收集依赖
if (isTracking()) {
// 需要进行依赖收集,直接调用trackEffect函数并将deps传递进去即可
trackEffect(this.deps)
}
return this._value
}
set value(newValue) {
// 判断修改的值是否和之前一致,如果一致直接返回,不进行触发依赖
if (this._value === newValue) return
this._value = newValue
triggerEffect(this.deps)
}
}
在RefImplement
类的实现中,我们使用了deps
作为依赖收集的容器。当需要收集依赖或者触发依赖时,出需要将这个容器作为参数传递给trackEffect
和triggerEffect
即可。
实现ref的嵌套属性响应式
ref
不仅仅可以将一个基本数据类型转成响应式,还可以将一个对象类型的数据变成响应式的。所以我们在RefImplement
类的constructor
中需要对传入的value
值进行判断。
// ref.spec.ts
it("should make nested properties reactive", () => {
// 接收一个对象
const a = ref({
count: 1,
});
let dummy;
effect(() => {
dummy = a.value.count;
});
expect(dummy).toBe(1);
// 值改变时
a.value.count = 2;
// dummy也发生变化
expect(dummy).toBe(2);
});
// ref.ts
import { reactive } from './reactive'
function isObject(value) {
reuturn value !== null && typeof value === 'object'
]
class RefImplement {
...
private raw
constructor(value) {
// raw用来保存value原始值,用于之后判断set的值是否和之前一样
this.raw = value
// conver函数用于判断当前value是否为对象
// 如果是对象则,将该对象包裹一层reactive,如果不是直接返回
this._value = conver(value)
this.deps = new Set()
}
get value() {
...
}
set value(newValue) {
if (isChange(this.raw, newValue)) return
this.raw = newValue
this._value = conver(newValue)
triggerEffect(this.deps)
}
}
function conver(value) {
return isObject(value) : reactive(value) : value
}
function isChange(raw, newValue) {
return raw === newValue
}
为了实现对象类型的响应式,需要在constructor
中判断传入得value
是否为对象,如果是对象,则将该对象使用reactive
函数,如果不是则直接返回。同时,还用一个raw
属性用来记录没被处理过的value,该属性主要是用来判断newValue和value是否相等。
实现isRef
isRef
是用来判断一个数据是否是ref
类型。
// ref.ts
class RefImplement {
public __v_isRef = true
}
export function isRef(value) {
if (!value) return false
return !!value.__v_isRef
}
实现isRef
其实非常简单,只需要在创建ref
实例的时候给ref实例添加上一个__v_isRef
属性即可。然后在isRef
函数中判断,是否有该属性即可。
实现unref
unRef
方法实际上就是一个语法糖,当一个值是ref
类型,那么unRef
会返回ref.value
。当一个值不是ref
类型,那么直接返回该值。
// ref.ts
...
export function unRef(ref) {
return isRef(ref) ? ref.value : ref
}
实现proxyRefs
proxyRefs
主要是用于对一个对象进行ref解包。在vue3中,我们在template中使用setup
函数返回的对象的属性值,不管属性值对应的值是不是ref类型,都不需要写.value。而vue3就是通过proxyRefs
来实现的。
// ref.spec.ts
it("proxyRefs", () => {
const user = {
age: ref(10),
name: "aaa",
};
const proxyUser = proxyRefs(user);
expect(user.age.value).toBe(10);
expect(proxyUser.age).toBe(10);
expect(proxyUser.name).toBe("aaa");
// 直接通过.age修改值
proxyUser.age = 20;
// proxyUser.age改变,user.age.value和proxy.age都需要改变
expect(proxyUser.age).toBe(20);
// 通过user.age.value还是能获取到更新后的值
expect(user.age.value).toBe(20);
proxyUser.age = ref(30);
expect(proxyUser.age).toBe(30);
expect(user.age.value).toBe(30);
});
// ref.ts
export function proxyRefs(objWithRef) {
return new Proxy(objWithRef, {
get(target, key, receiver) {
return unRef(Reflect.get(target, key, receiver))
},
set(target, key, newValue, receiver) {
if (isRef(target[key]) && !isRef(newValue)) {
return Reflect.set(target, key, ref(newValue), receiver)
}
return Reflect.set(target, key, newValue, receiver)
}
})
}
实际上proxyRefs
就是返回一个proxy
对象,在getter
中,只需要通过unRef
对获取值时进行解包。而在setter
操作时,就需要注意一下。当出现修改的新值不是ref类型,而之前这个值是ref类型的情况时,需要将该新值变成ref类型。