【Vue3源码】第六章 ref的原理 实现ref
上一章节我们实现了reactive 和 readonly 嵌套对象转换功能,以及shallowReadonly 和isProxy几个简单的API。
这一章我们开始实现 ref 及其它配套的isRef、unRef 和 proxyRefs
我们看下三行简单的代码
const a = ref(1);
effect(() => {
calls++;
dummy = a.value;
});
a.value = 2;
接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value
。
ref 对象是可更改的,也就是说你可以为 .value
赋予新的值。它也是响应式的,即所有对 .value
的操作都将被追踪,并且写操作会触发与之相关的副作用。
如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。
首先还是先看单元测试,我们测试代码去测试ref的功能是否齐全
在tests文件夹下新建一个ref.spec.ts文件
import { effect } from "../effect";
import { ref } from "../ref";
describe("ref", () => {
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);
// same value should not trigger
a.value = 2;
expect(calls).toBe(2);
expect(dummy).toBe(2);
});
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;
expect(dummy).toBe(2);
});
});
ref其实就是为了给基础类型绑定响应式而创建的(如果是引用类型它会被转为reactive),它的原理和vue2的Object.defineProperty()类似。
同理的ref内也有和Proxy类似的get和set捕获器,不过ref主要是通过类实现的,所以ref的get和set叫类修饰符更贴切一点。他们运行的逻辑也很相似,都是在get时去收集依赖,set时触发依赖。
那么ref和reative原理最大区别是什么呢?
是的不管track还是trigger两个函数他们都接收target和key两个参数,而ref我在上文说了它为了给基础类型绑定响应式而创建的。所以操作target和key的操作我们就可以省略了。
我们的ref在get时直接添加dep即可
//依赖收集
export function track(target, key) {
if (activeEffect && shouldTrack) {
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);
}
trackEffects(dep)
}
}
export function trackEffects(dep) {
if(dep.has(activeEffect)) return
dep.add(activeEffect);
//浅拷贝反向收集到dep
activeEffect.deps.push(dep);
}
我们的trigger,也直接操作dep即可
//依赖触发
export function trigger(target, key) {
let depsMap = targetMap.get(target);
//用stop时所有的dep都被删了
let dep = depsMap.get(key);
triggerEffects(dep)
}
export function triggerEffects(dep) {
for (let effect of dep) {
// 当触发set时,如果有scheduler就执行scheduler
if (effect.scheduler) {
effect.scheduler();
// 没有就触发ReactiveEffect实例的run方法
} else {
effect.run();
}
}
}
做完上面的操作我们已经抽离出了
在reactivity文件夹下
新建ref.ts文件
import { hasChanged, isObject } from "../shared"
import { trackEffects, triggerEffects, activeEffect, shouldTrack } from "./effect"
import { reactive } from "./reactive"
class RefImpl {
private _value: any //劫持的数据
public dep //收集dep
private _rawValue:any //劫持的数据备份
constructor(value) {
// 初始化 value
this._rawValue = value
// 如果是个对象就用reative包裹
this._value = convert(value)
// 初始化空的dep
this.dep = new Set()
}
get value() {
// 收集activeEffect实例
trackRefValue(this)
// 返回当前劫持的数据
return this._value
}
set value(newValue) {
// 要先修改value值再触发依赖
// 如果修改过的值和初始化的值不相同
if (hasChanged(newValue,this._rawValue)) {
// 给初始化的值赋给成新的值
this._rawValue = newValue
// 如果是reactive对象也替换新值
this._value = convert(newValue)
// 去触发依赖
triggerEffects(this.dep)
}
}
}
function convert(value) {
return isObject(value) ? reactive(value) : value
}
function trackRefValue(ref) {
if (activeEffect && shouldTrack) {
trackEffects(ref.dep)
}
}
export function ref(value) {
return new RefImpl(value)
}
shared文件夹下
index.ts文件
export const hasChanged = (val,newVal) => {
return !Object.is(val,newVal)
}
isRef()
检查某个值是否为 ref。
unref()
如果参数是 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val
计算的一个语法糖。
it("isRef", () => {
const a = ref(1);
const user = reactive({
age: 1,
});
expect(isRef(a)).toBe(true);
expect(isRef(1)).toBe(false);
expect(isRef(user)).toBe(false);
});
it.skip("unRef", () => {
const a = ref(1);
expect(unRef(a)).toBe(1);
expect(unRef(1)).toBe(1);
});
实现起来非常的简单,我们怎么检查某个值是否为 ref呢?只要在RefImpl添加一个公共属性__v_isRef = true
这样我们就可以通过访问实例中ref.__v_isRef是否存在来鉴定某个值是否为 ref~
有了isRef,我们就可以实现unRef了,这是 val = isRef(val) ? val.value : val
计算的一个语法糖。
import { hasChanged, isObject } from "../shared"
import { trackEffects, triggerEffects, activeEffect, shouldTrack } from "./effect"
import { reactive } from "./reactive"
class RefImpl {
private _value: any
public dep
private _rawValue:any
//新增
public __v_isRef =true
constructor(value) {
this._rawValue = value
this._value = convert(value)
this.dep = new Set()
}
get value() {
trackRefValue(this)
return this._value
}
set value(newValue) {
// 要先修改value值再触发依赖
if (hasChanged(newValue,this._rawValue)) {
this._rawValue = newValue
this._value = convert(newValue)
triggerEffects(this.dep)
}
}
}
export function ref(value) {
return new RefImpl(value)
}
export function isRef(ref) {
return !! ref.__v_isRef
}
export function unRef(ref) {
return isRef(ref) ? ref.value : ref
}
看见proxyRefs你可能会觉得奇怪Vue3文档中并没有这个API,proxyRefs的功能主要是实现了不需要.value去访问ref对象。
it("proxyRefs", () => {
const user = {
age: ref(10),
name: "xiaohong",
};
const proxyUser = proxyRefs(user);
expect(user.age.value).toBe(10);
expect(proxyUser.age).toBe(10);
expect(proxyUser.name).toBe("xiaohong");
(proxyUser as any).age = 20;
expect(proxyUser.age).toBe(20);
expect(user.age.value).toBe(20);
proxyUser.age = ref(20);
expect(proxyUser.age).toBe(20);
expect(user.age.value).toBe(20);
});
proxyRefs有什么用呢?
例如我们在template中访问ref对象时其实是不用.value去访问的。因为proxyRefs帮我们在get时做了处理让他返回成普通值,set时也做了处理让它返回普通值即可。
export function proxyRefs(objectWithRefs) {
return isReactive(objectWithRefs) ? objectWithRefs : new Proxy(objectWithRefs,{
get(target,key) {
// get 如果是ref类型那么就返回.value的值
// 如果是普通的值直接返回
return unRef(Reflect.get(target,key))
},
set(target,key,value) {
// 判断旧值是不是ref,新值是ref还是普通类型
if(isRef(target[key]) && !isRef(value)) {
// 普通类型就替换成普通类型
return target[key].value = value
}else {
// 是ref就返回.value的值
return Reflect.set(target,key,value)
}
}
})
}
下期分享:实现computed