likes
comments
collection
share

计算属性为何有缓存功能?

作者站长头像
站长
· 阅读数 24

前言

官方文档 computed的定义:

接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。

另外,众所周知,计算属性具有缓存功能,因此我们可以将实现computed()方法的需求归纳为:

  1. computed接受的一个fn,返回值要用.value来访问
  2. computed缓存功能,当它的响应式依赖更新时才会重新执行fn,否则不会被调用。

单测

找来源码中,满足这两点需求的单测。新建computed.spec.ts

import { computed } from "../computed";
import { reactive } from "../reactive";

describe("computed", () => {
  it("should return updated value", () => {
    const value = reactive({});
    const cValue = computed(() => value.foo);
    expect(cValue.value).toBe(undefined);
    value.foo = 1;
    expect(cValue.value).toBe(1);
  });

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

    // lazy
    expect(getter).not.toHaveBeenCalled();

    expect(cValue.value).toBe(undefined);
    expect(getter).toHaveBeenCalledTimes(1);

    // should not compute again
    cValue.value;
    expect(getter).toHaveBeenCalledTimes(1);

    // should not compute until needed
    value.foo = 1;
    expect(getter).toHaveBeenCalledTimes(1);

    // now it should compute
    expect(cValue.value).toBe(1);
    expect(getter).toHaveBeenCalledTimes(2);

    // should not compute again
    cValue.value;
    expect(getter).toHaveBeenCalledTimes(2);
  });
});

首先,先实现第一个测试用例should return updated value应该返回更新后的值。

实现第 1 个测试用例

将第二个测试用例跳过,添加skip方法,it.skip("should compute lazily",...)

第一个测试用例中,定义了一个空的响应式对象value,计算属性返回其中的foo属性,那第一次访问计算属性值的时(通过.value形式)应该获得undefined。当响应式对象更新了foo属性的值为 1,计算属性也做相应的修改。

新建computed.ts

class ComputedRefImpl {
  private _getter: any;
  constructor(getter) {
    this._getter = getter
  }
  get value() {
    return this._getter();
  }
}

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

computed返回值需要通过.value,实现起来和ref一样,都通过class类来进行对象模拟,可以实现数据拦截。其返回值就是传入的getter函数的执行结果。

如果此时执行单测yarn test computed就会发现第 1 个断言expect(cValue.value).toBe(undefined)成功通过,报错发生在第 2 个断言,因为我们还未处理响应式更新之后的逻辑。

可以分析一下,当响应式对象valuefoo更新为 1,需要计算属性相应的返回 1。那相当于依赖的value在更新时computed也会触发更新。那此前实现的响应式依赖处理的函数就是effect,这里就可以复用一下。

effect.tsReactiveEffect类导出,修改上面的ComputedRefImpl

import { ReactiveEffect } from "./effect";
class ComputedRefImpl {
  private _effect: any;
  constructor(getter) {
    this._effect = new ReactiveEffect(getter);
  }

  get value() {
    return this._effect.run();
  }
}

当响应式数据value更新时会触发它的trigger方法,trigger方法中会将dep中所有effect执行,此时执行了effect.run()方法再将这个结果返回到computedget value中,也就是顺利的实现了同步更新。

执行单测yarn test computed,第一个测试用例通过。

计算属性为何有缓存功能?

实现第 2 个测试用例

放开skip,直接执行单测,

计算属性为何有缓存功能?

发现问题,解决问题。

首先来分析一下这个测试用例,具体都测试了些什么。

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

  // lazy
  expect(getter).not.toHaveBeenCalled();

  expect(cValue.value).toBe(undefined);
  expect(getter).toHaveBeenCalledTimes(1);

  // should not compute again
  cValue.value;
  expect(getter).toHaveBeenCalledTimes(1);

  // should not compute until needed
  value.foo = 1;
  expect(getter).toHaveBeenCalledTimes(1);

  // now it should compute
  expect(cValue.value).toBe(1);
  expect(getter).toHaveBeenCalledTimes(2);

  // should not compute again
  cValue.value;
  expect(getter).toHaveBeenCalledTimes(2);
});

定义了一个空的响应式对象valuejest模拟了一个computed内的getter函数。当没有访问computed值时,getter函数不会被调用;当访问computed值时,会被调用 1 次;再次去访问computed值时,getter不会被调用,调用次数仍然是 1 ; 响应式数据value更新,getter仍然不会被触发;但再去访问计算属性时,getter会被调用获取最新值;再次访问时就不再调用了。

具体实现:

import { ReactiveEffect } from "./effect";

class ComputedRefImpl {
  private _effect: any;
  private _dirty: boolean = true;
  private _value: any;
  constructor(getter) {
    this._effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true;
      }
    });
  }
  get value() {
    if (this._dirty) {
      this._dirty = false;
      this._value = this._effect.run();
    }
    return this._value;
  }
}

使用一个布尔值变量_dirty来控制何时可以去访问computed值,也就get value中的_effect.run()的触发,调用过一次就关闭_dirty表示下一次不给触发了,并将结果缓存起来,不调用执行effect时直接返回这个缓存值。再借助ReactiveEffect类中scheduler变量,存在scheduler时触发trigger中执行的就是scheduler的函数,就可以在这个函数里操作_dirty再次开启,以便下次访问computed时会继续触发get value,从而更新计算属性的值和响应式数据保持一致。

执行单测yarn test computed,测试通过。

计算属性为何有缓存功能?