实现computed
mini-vue3的同步代码实现点击这里
mini-vue3的所有文章点击这里
computed的基本实现
computed
可以接收一个getter
函数,当getter
函数内依赖的响应式数据发生改变时,getter
函数会重新执行。同时也可以接收一个对象,在对象里面添加get属性和set属性,基于之前实现的ReactiveEffect我们可以比较简单的实现这个功能。
实现computed传入getter函数
// computed.spec.ts
import { reactive } from "../reactive";
import { computed } from "../computed";
import { ref } from "../ref";
describe("computed", () => {
it("happy path", () => {
const obj = reactive({
age: 10,
});
const cValue = computed(() => {
return obj.age;
});
expect(cValue.value).toBe(10);
});
it("should compute layily", () => {
const value = reactive({
foo: 1,
});
const getter = jest.fn(() => value.foo);
const cValue = computed(getter);
// lazy
// 当没有调用cValue.value时,是不会调用getter函数的
expect(getter).not.toHaveBeenCalled();
expect(cValue.value).toBe(1);
expect(getter).toHaveBeenCalledTimes(1);
// 多次获取.value不会重新调用getter
cValue.value;
expect(getter).toHaveBeenCalledTimes(1);
// 修改依赖值
value.foo = 2;
// 没有.value所以不会执行getter
expect(getter).toHaveBeenCalledTimes(1);
expect(cValue.value).toBe(2);
expect(getter).toHaveBeenCalledTimes(2);
cValue.value;
expect(getter).toHaveBeenCalledTimes(2);
});
});
复制代码
我们想要实现的基础功能如上面的测试代码所示。computed的getter函数是懒执行的。也就是说如果我们没有调用.value
去获取该值时,是不会执行getter函数的。而且当依赖的值没有发生变化时,无论调用多少次.value都不会重新执行getter函数。只有当依赖的值发生变化后,第一次调用.value,这个时候的getter才会重新执行。
// computed
import { ReactiveEffect } from './effect'
class ComputedRefImpl {
// effect实例
private _effect
// 用于记录是否需要重新执行getter函数
private_dirty = true
private _value
constructor(getter) {
this._effect = new ReactiveEffect(getter, () => {
this._dirty = true
})
}
get value() {
if (this._dirty) {
this._value = this._effect.run()
this._dirty = false
}
return this._value
}
}
export function computed(getter) {
return new ComputedRefImpl(getter)
}
复制代码
在上面的实现中,我们使用了ReactiveEffect
这个类创建了effect实例,同时将getter函数以及一个将dirty变为true的函数传递给了ReactiveEffect。在ReactiveEffect
的实现中,如果我们有传入第二个参数也就是scheduler
,那么在触发依赖时,会执行这个scheduler函数。而当我们进行.value
操作时,会进入到get
函数,在get
函数中就,会判断当前的dirty
是否为true,如果为true,那么会执行this._effect.run
方法,其实也就是会执行getter
方法,而执行了getter
方法我们就能够获取到getter
方法的返回值,在执行this._effect.run
方法后会将dirty 变为false。这样下次进行.value
时就不会再次执行getter方法。而getter
依赖的响应式数据发生变化时,那么会执行我们传入的scheduler
函数,在scheduler
函数中,会将dirty 设置为true。这样在下次进行.value
的时候就又会执行getter
方法,获取到最新的值。
实现computed同时传入getter和setter
下面的代码就是computed函数接收一个既有get
又有set
的对象。当plusOne的值发生变化时,会执行set
方法。
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value)
复制代码
想要实现上面的功能,首先就要会传入的参数进行类型判断。然后在给ComputedRefImpl
这个类加上get value
的方法即可。
// computed.ts
class ComputedRefImpl {
private _setter
constructor(getter, setter?) {
this._setter = setter
...
}
...
set value(newValue) {
// 将this._value赋值为新的值
this._value = newValue
// 判断setter是否为函数,如果是直接调用即可
if (typeof this._setter === 'function') {
this._setter(newValue)
}
}
}
export function computed(getter) {
let get, set
// 判断参数是否为对象类型
if (isObject(getter)) {
get = getter.get
set = getter.set
} else {
get = getter
}
return new ComputedRefImpl(get, set)
}
复制代码
在computed
中,对传入的参数进行判断。如果传入的参数为对象,那么就将get
和set
赋值为参数对象的get属性和set属性。然后将get
和set
作为ComputedRefImpl
的参数传递进去。在ComputedRefImpl
中,只需要将getter方法保存起来,然后在set value
里面进行调用即可。