【Vue3源码】第六章 computed的实现
上一章节我们实现了 ref 及其它配套的isRef、unRef 和 proxyRefs API。这一章开始实现computed计算属性。
认识computed
接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value
暴露 getter 函数的返回值。它也可以接受一个带有 get
和 set
函数的对象来创建一个可写的 ref 对象。
computed计算属性最重要的功能就是:仅会在其响应式依赖更新时才重新计算。
为什么需要缓存呢?想象一下我们有一个非常耗性能的计算属性
list
,需要循环一个巨大的数组并做许多计算逻辑,并且可能也有其他计算属性依赖于list
。没有缓存的话,我们会重复执行非常多次list
的 getter,然而这实际上没有必要!如果你确定不需要缓存,那么也可以使用方法调用。
单元测试代码
首先看单元测试代码:
我们需要实现懒计算(响应式依赖更新时才重新计算)
所以我们测试内容主要需要实现的步骤有以下三步:
- 当getter作为参数传入computed时,不会触发计算
- 当访问计算属性的返回值cValue会被触发一次,响应式依赖如果不进行更新,重复访问时不会再次触发计算
- 响应式依赖更新时才重新计算
import { computed } from "../computed";
import { reactive } from "../reactive";
describe("computed", () => {
it("happy path", () => {
const value = reactive({
foo: 1,
});
const getter = computed(() => {
return value.foo;
});
value.foo = 2;
expect(getter.value).toBe(2);
});
it("should compute lazily", () => {
const value = reactive({
foo: 1,
});
const getter = jest.fn(() => {
return value.foo;
});
const cValue = computed(getter);
// lazy
expect(getter).not.toHaveBeenCalled();
expect(cValue.value).toBe(1);
expect(getter).toHaveBeenCalledTimes(1);
// should not compute again
cValue.value;
expect(getter).toHaveBeenCalledTimes(1);
// should not compute until needed
value.foo = 2;
expect(getter).toHaveBeenCalledTimes(1);
// now it should compute
expect(cValue.value).toBe(2);
expect(getter).toHaveBeenCalledTimes(2);
// should not compute again
cValue.value;
expect(getter).toHaveBeenCalledTimes(2);
});
});
实现代码
计算属性是基于现有响应式对象而衍生出来的,它的实现代码中就有创建ReactiveEffect对象的流程。
computedRefImpl类是computed API的核心。
从ReactiveEffect的构造方法和计算属性创建ReactiveEffect对象源码可知,构造函数的fn回调方法就是getter方法,而schedule方法就是设置计算属性为脏数据的匿名方法。
import { ReactiveEffect } from "./effect";
class computedRefImpl {
private _getter: any; //保存getter
private _dirty: boolean = true; //将数据标记为藏数据控制计算属性更新
private _value: any; // 保存run方法返回的value值
private _effect: any; //保存ReactiveEffect实例
constructor(getter) {
this._getter = getter;
//通过ReactiveEffect类中的scheduler选项实现每次new ReactiveEffect才能更新计算值
this._effect = new ReactiveEffect(getter, () => {
// 通过_dirty实现依赖触发更新内容
if (!this._dirty) {
// 标记脏数据
this._dirty = true;
}
});
}
get value() {
// get => get value
// 当依赖的响应式的对象的值发生改变的时候才会更新
// effect
if (this._dirty) {
this._dirty = false;
// 获取到 fn 的返回值
this._value = this._effect.run();
}
return this._value;
}
}
export function computed(getter) {
return new computedRefImpl(getter);
}
-
constructor
构造函数新建了一个ReactiveEffect实例,所有的响应式都是通过这个对象来实现的,实例中我们通过ReactiveEffect类中scheduler的设计,停止了run方法的执行转而执行我们scheduler中传入的匿名函数。
- 第一个参数是getter方法,这个方法会进行依赖收集和数据计算。
- 第二个方法则是scheduler依赖变更时的回调方法,这个方法将数据标识为脏数据,表示下次读取时需要重新计算,并且触发依赖更新。
是的之前的文章《【Vue3源码】第二章 effect功能的完善上》有埋下scheduler实现的伏笔,它在computed中收回~
-
get value()
这是计算属性的重点,核心逻辑是判断当前缓存数据是否脏数据,是脏数据就重新计算。其中ReactiveEffect对象的run方法会调用getter方法进行计算。
computed流程图
到了这里,reactivity文件夹的学习就告一段落了下一part开始学习runtime-core文件夹
下节预告:初始化 component 主流程