手写mini-vue3:实现reactivity模块中的-computed

实现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中,对传入的参数进行判断。如果传入的参数为对象,那么就将getset赋值为参数对象的get属性和set属性。然后将getset作为ComputedRefImpl的参数传递进去。在ComputedRefImpl中,只需要将getter方法保存起来,然后在set value里面进行调用即可。

猜你喜欢

转载自juejin.im/post/7066270655842353166