实现 computed

在本小节中,将会实现 computed

1. happy path

先看测试

it("happy path", () => {
  const user = reactive({
    age: 1,
  });

  const age = computed(() => {
    return user.age;
  });

  expect(age.value).toBe(1);
});

接下来我们看如何实现

class ComputedRefImpl {
  private _getter: any;
  constructor(getter) {
    this._getter = getter;
  }
  get value() {
    // 在调用 value 时将传入的 getter 的执行结果返回
    return this._getter();
  }
}

export function computed(getter) {
  return new ComputedRefImpl(getter);
}

2. 缓存机制

我们来看看测试样例

it("should computed lazily", () => {
  const value = reactive({ foo: 1 });
  const getter = jest.fn(() => value.foo);
  const cValue = computed(getter);

  // lazy
  expect(getter).not.toHaveBeenCalled();
  // 触发 get 操作时传入的 getter 会被调用一次
  expect(cValue.value).toBe(1);
  expect(getter).toHaveBeenCalledTimes(1);

  // 不会再次调用 computed
  cValue.value;
  expect(getter).toHaveBeenCalledTimes(1);
});

这里我们发现,再次读取 cValue.value 的时候是不会再次去计算的,而是拿的缓存。

class ComputedRefImpl {
  private _getter: any;
  // _value 缓存值
  private _value: any;
  // _dirty 是否需要更新值
  private _dirty = true;
  constructor(getter) {
    this._getter = getter;
  }
  get value() {
    // 这里进行判断,如果还未初始化,执行 getter,缓存一份
    if (this._dirty) {
      this._value = this._getter();
      this._dirty = false;
    }
    // 这里就直接返回缓存
    return this._value;
  }
}

export function computed(getter) {
  return new ComputedRefImpl(getter);
}

接下来,我们来看看进阶版的

// 在不需要这个 computed 的时候 value 变了 computed 也不会执行
value.foo = 2;
expect(getter).toHaveBeenCalledTimes(1);

// 在需要这个 computed 的时候再次计算(如果 computed 依赖的值已经发生更改)
expect(cValue.value).toBe(2);
expect(getter).toHaveBeenCalledTimes(2);

// 不变拿的就是缓存
cValue.value;
expect(getter).toHaveBeenCalledTimes(2);

其实现在我们就已经有了思路了,为什么不执行 getter 是因为我们加了一把锁 _dirty,那么只需要在依赖的值所发生的改变的时候将这个 _dirty = true 就可以了,那么再次 get value 的时候就会因为锁打开了而重新执行并计算值

我们知道依赖的值发生改变的时候其实是进入了 trigger 方法里面,而 trigger 中有一个判断条件,那就是如果 ReactiveEffect option 有 scheduler 的话,是会执行 scheduler 而不是 execution,那么我们就可以通过这个 scheduler 做文章了。

那么现在问题又来了,scheduler 是存在 EeactiveEffect 实例上的,而该类是在 effect 中创建实例的,所以我们的 computed 其实也需要自己维护一个 effect,相当于把 getter 作为 effect。

import { ReactiveEffect } from "./effect";

class ComputedRefImpl {
  private _getter: any;
  private _value: any;
  private _dirty = true;
  private _effect: any;
  constructor(getter) {
    this._getter = getter;
    // 这里需要内部维护一个 ReactiveEffect 实例
    this._effect = new ReactiveEffect(getter, {
      scheduler: () => {
        // 在 scheduler 中把锁打开
        this._dirty = true;
      },
    });
  }
  get value() {
    // 因为在依赖值更新的时候会进行 triiger, triiger 调用 scheduler,锁打开了
    // 再次 get value,因为锁是打开的,就可以重新计算值了
    if (this._dirty) {
      this._value = this._effect.run();
      this._dirty = false;
    }
    return this._value;
  }
}

export function computed(getter) {
  return new ComputedRefImpl(getter);
}

这样我们的测试就跑通了