分享:我欲与君相知,长命无绝衰。山无棱,江水为竭,冬雷震震,夏雨雪,天地合,乃敢与君绝!——《上邪》
以后的每篇都分享一下喜欢的文字(小说段落,诗词等等,兄弟萌有喜欢的也可以给我分享一下!)
前言
相信各位大佬在使用Vue的时候都用过Computed 计算属性这个响应式核心API,那么我们对它的奇妙之处,神奇之处了解多少呢?想要搞清楚它是什么,首先就需要知道它的神奇之处(Vue2与Vue3实现有所不同,以Vue3为主) ps:Vue2源码没看😏:
- 它的返回值是一个响应式ref对象
- 它接收一个getter函数,具有缓存性,当依赖没改变的时候会执行返回缓存值,依赖改变会重新计算再返回并且缓存当前值
只要我们搞懂了这两点,Computed岂不是手到擒来
“神奇”的功能性
我们知道了计算属性的神奇,想想如何实现这样的“神奇”,所以我们先用测试代码写出我们要实现出来的功能,然后再根据功能实现代码
- 返回值:响应式ref对象
const 陪我去看海 = reactive({
age: 22,
});
const age = computed(() => {
return 陪我去看海.age;
});
expect(age.value).toBe(22);
- 缓存性
const value = reactive({
foo: 22,
});
const getter = jest.fn(() => {
return value.foo;
});
// 当数据被get(获取)的时候,执行一次getter,多次get,也执行一次
const cValue = computed(getter);
expect(getter).not.toHaveBeenCalled();
expect(cValue.value).toBe(22);
expect(getter).toHaveBeenCalledTimes(1);
cValue.value;
expect(getter).toHaveBeenCalledTimes(1);
// 返回的是响应式对象,根据响应式原理,set的时候触发依赖函数,这里依赖函数的主体就是getter
value.foo = 23;
expect(getter).toHaveBeenCalledTimes(1);
expect(cValue.value).toBe(23);
expect(getter).toHaveBeenCalledTimes(2);
cValue.value;
expect(getter).toHaveBeenCalledTimes(2);
“神奇”的实现
- 返回ref对象
class ComputedRefImpl {
private _getter: any;
private _value: any;
constructor(getter) {
this._getter = getter;
}
get value() {
this._value = this._getter();
return this._value;
}
}
export function computed(getter) {
return new ComputedRefImpl(getter);
}
computed是一个函数,参数也是一个函数,内部实现使用一个class实现,让它返回一个类的实例,computed的返回值,就是getter函数的返回值,当用户传递函数进来后,使用 _getter 属性进行存储,使用 _value 属性进行存储值,最后当每次用户调用get value的时候取到值,相当于模拟ref的使用方法。
- 缓存性
class ComputedRefImpl {
private _getter: any;
private _dirty: boolean = true;
private _value: any;
private _effect: any;
constructor(getter) {
this._getter = getter;
// 这里执行顺序,先schedule再getter,也就是在set的时候,先打开开关,再执行getter
this._effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true;
}
});
}
get value() {
/*
当值没有改变,直接取存储的值
一直调用get,不会重新计算,只有在set后才会重新计算值
*/
if (this._dirty) {
this._dirty = false;
this._value = this._effect.run();
}
return this._value;
}
}
export function computed(getter) {
return new ComputedRefImpl(getter);
}
PS:这里 class ReactiveEffect 先理解成相当于包装一下这个函数,让它被收集进Dep,执行run的时候就是执行它自己,后面会讲到,暂时这样理解就行
这里我们新增了两个私有属性 _dirty, _effect ;
- dirty是用来作为是否需要重新计算的开关;
- effect是用来将用户传入进来的函数通过ReactiveEffect包装变为依赖函数并存储;
这里也就是说,当第一次被get的时候,它dirty初始值是true,所以执行getter并关闭dirty,让后面的get取值的时候取的是 _value 存储的值,这里就实现了缓存的特性。但是问题就是当被set后,我们需要更新后的值,所以当set的时候,我们需要打开 _dirty 让其重新计算,这里我们是使用到了另外一个类ReactiveEffect来实现。接下来看看它主要实现是如何的
class ReactiveEffect
// 用来包装依赖函数的类
export class ReactiveEffect {
private _fn;
public scheduler: Function | undefined;
constructor(fn, scheduler?: Function) {
this._fn = fn;
this.scheduler = scheduler;
}
run() {
activeEffect = this;
// 在这里会执行 track
const result = this._fn();
// 返回 runner 执行后的值
return result;
}
}
可以看到,这里其实就是相当于给getter函数穿了一层衣服,主要目的就是为了这个scheduler,通过这个来改变是否需要重新计算的状态,以及把函数收集进入依赖容器,为后面set的时候执行scheduler埋下伏笔,这里响应式逻辑就把相关的逻辑挑出来看下了(详细得自己去了解一下,或者看一下我的另外一篇文章),了解响应式就了解两个逻辑就好(如何收集?如何执行?)
如何收集?如何执行?
- 如何收集?
export function trackEffect(dep) {
if (dep.has(activeEffect)) return;
dep.add(activeEffect);
// dep 存 activeEffect, activeEffect反向存储dep
activeEffect.deps.push(dep);
}
这里 dep 就是一个容器,至于具体是什么,不用管,他的目的就是将 activeEffect 收集进 dep 中存储,该函数会在get的时候执行
- 如何执行?
export function triggerEffects(dep) {
// 注意:这里是对象遍历,所以这两个属性如果有,都会执行,只有先后顺序
for (const effect of dep) {
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
}
dep就是那个收集依赖的容器,然后遍历这个容器取出所有相关依赖对象,当有scheduler的时候先执行scheduler,没有的时候直接执行run也就是函数本身,该函数会在set的时候执行
这样后,我们再把前后的知识串联起来看,就实现出了一个简易版的Computed
知识串联总结
- computed 内部,在取值的时候会有一个开关,true的时候,重新计算了值,并存储到一个私有属性上,false的时候,直接取上一次存储进去的值(必然会有一次执行,那就是初始化)
- 开关更改的时机只有一个地方,那就是在set更新值之后,将开关打开,在下一次get的时候重新计算
- 当set的时候都会执行一个trigger函数,该函数会把收集起来的对应依赖遍历执行,在执行的时候会先执行scheduler,然后执行函数本身
这个功能属于Vue中响应式核心的一部分,所以如果想要更好的明白细节的话,需要先了解响应式原理。