vue3源码学习-7-computed的实现

前言

回顾上期的内容,编写了 effect 中的调度器,主要修改了 effect.ts 文件。在预览之前的代码的时候会发现一些优化的地方。

在 vue 代码的需求编辑中,会遇到这样一个例子。例如一个人的姓名分为姓和名,那么我希望在页面上打印出这个人的姓+名,而且在姓或者名改变的时候,页面渲染也会改变。那么就用到了 vue 的 computed 来进行操作。旧版的 vue2 中 computed 是基于 watcher 实现的。vue3 则是基于 effect 来实现。另外 vue3 中的 computed 写法叫组合式 API,而 vue2 是拿 data 中的属性来编写 computed 中的属性,这种叫选项式 API(option)。具体的 vue3 写法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const { effect, reactive, computed } = VueReactivity;
let target = { firstName: "张", lastName: "四" };
const state = reactive(target);
let app = document.getElementById("app");
const fullName = computed(() => {
console.log("runner");
return state.firstName + state.lastName;
});
effect(() => {
return (app.innerHTML = fullName.value);
});

setTimeout(() => {
state.firstName = "王";
}, 1000);

也可以写成如下的方式

1
2
3
4
5
6
7
8
9
10
const fullName = computed({
get() {
console.log("runner");
return state.firstName + state.lastName;
},
set() {},
});
fullName.value;
fullName.value;
fullName.value;

并且在多次访问 value 的时候,如果值未改变就不触发运行,也就是说带有一个缓存的功能。

baseHandle 功能优化

在给属性包裹一层代理的时候,如果对象的属性还是一个对象之类的属性,那么返回的不应该只是这个值,而应该是这个对象代理之后的值。

1
2
3
4
5
6
// baseHandle.ts get访问器中判断是否是对象,如果是就再代理一层
let res = Reflect.get(target, key, recevier);
if (isObject(res)) {
return reactive(res); // 深度代理实现
}
return res;

编写 computed 功能

对着上面的官方用法,知道如果只有一个函数的话,那就是这个函数默认为 get,还可以有一个对象的写法,那就是将用户的 set 和 get 赋值上去。
另外还 effect 中渲染了 computed 抛出的值,然后这个值改变了,也触发了 effect 的 run,让他重新渲染,这说明 computed 是记录了 effect 的依赖的。
上文说过 computed 是基于 effect,实际上它和 effect 基本相等。那么就出现了 effect 包裹了 effect 这种写法。

48a2d11a721ffa60acaac.png

也就是这样传递着改变的信息,来触发页面渲染。

第一步创建 computed.ts 基础

判断传递过来的式函数还是对象,然后就获取传递的 get 和 set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export const computed = (getterOrOptions) => {
let onlyGetter = isFunction(getterOrOptions);
let getter: Function, setter: Function;

if (onlyGetter) {
getter = getterOrOptions;
setter = () => {
console.warn("no set");
};
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}

return new ComputedRefImpl(getter, setter); // 将拿到的getter和setter
};

编写 ComputedRefImpl 类

这个类主要的操作是
第一个赃值检测,就是 value 值多次 get,然后值还没改变。
第二个是搜集 effect,这样值更新通知对应的 effect 进行用户的函数回调
第三个如果不是赃值,那就运行调度函数,就是用户的操作函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class ComputedRefImpl {
public effect: ReactiveEffect;
public _dirty: boolean = true; // ,默认应该取值的时候进行计算
public __v_isReadonly: boolean = true;
public __v_isRef: boolean = true;
public _value: any;
public dep = new Set();
constructor(public getter: Function, public setter: Function) {
// 我们将用户的getter放到effect中,这里面的属性会被这个effect收集起来
this.effect = new ReactiveEffect(getter, () => {
//稍后依赖的属性变化会执行这个调度函数

if (!this._dirty) {
this._dirty = true;

// 实现一个触发更新
triggerEffect(this.dep);
}
});
}

//类的属性访问器, 底层就是Obeject.defineProperty
get value() {
// 做依赖收集
trackEffect(this.dep);
if (this._dirty) {
this._dirty = false; // 第一次取过之后的的时候才设置为false
// 说明这个值是赃值
this._value = this.effect.run();
}

return this._value;
}

set value(newValue) {
this.setter(newValue);
}
}

上面代码中的 triggerEffect 和 trackEffect 是对之前的 effect 代码做了一个函数功能分离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 这不就是之前代码中的依赖收集和函数回调操作吗
export function trackEffect(dep: any) {
if (activeEffect) {
let shouldTrack = !dep.has(activeEffect); //一个属性多次依赖同一个effect那么去重
if (shouldTrack) {
dep.add(activeEffect);
activeEffect.deps.push(dep); // 让deps记录住对应的dep,稍后在清理的地方用到
}
}
}

export function triggerEffect(effects) {
effects = new Set(effects);
effects.forEach((effect) => {
if (activeEffect !== effect) {
if (effect.schedule) {
effect.schedule(); // 用户传入schedule的时候,就调用回调
} else {
effect.run(); // 否则就刷新
}
}
// 如果这里直接就写effect.run(),那么会遇到这种情况,在模版中赋值,那么也会触发这个,
// 然后又通过了依赖收集的时候,运行它的第一次run()。就会导致循环调用,爆栈,
//所以这里需要加一个判断是否是当前的effect,如果是的话,就忽略这一次的赋值触发的run();
//注意目前的代码是不支持异步的
});
}

结尾

经过上面的代码编写之后就能得到一个自己的 computed 函数,可以试验下,发现能得到相应的效果

1
git:[@github/MicroMatrixOrg/vue3-plan/tree/add-computed]