手撸mini-vue之computed

196 阅读2分钟
happy path 单元测试
// computed.spec.ts

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

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

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

以上是一个简单的 computed get 操作

代码实现
// 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);
}
should compute lazily 单元测试
// computed.spec.ts

describe("computed", () => {
  it("should compute lazily", () => {
    const value = reactive({ foo: 1 });
    const getter = jest.fn(() => {
      return value.foo;
    });
    const cValue = computed(getter);
    
    // 断言1
    // 懒执行 不调用 cValue 不会执行getter
    expect(getter).not.toHaveBeenCalled();

    expect(cValue.value).toBe(1);
    expect(getter).toHaveBeenCalledTimes(1);
    
    // 断言2
    // 缓存机制 再次调用cValue.value getter不再执行
    cValue.value;
    expect(getter).toHaveBeenCalledTimes(1);
    
    // 断言3
    value.foo = 2;
    expect(getter).toHaveBeenCalledTimes(1);

    // 当依赖的响应式对象的值发生改变 再次调用cValue.value  getter执行
    expect(cValue.value).toBe(2);
    expect(getter).toHaveBeenCalledTimes(2);

    cValue.value;
    expect(getter).toHaveBeenCalledTimes(2);
  });
})
断言1

从断言1可以知道, computed 是懒执行,只有当调用 cValue.value 时才会执行 getter
用我们的上面的 happy path 单元测试的实现代码就可以通过

断言2

从断言2可以知道 computed 具有缓存机制,当 cValue.value 重复调用的时候 getter 始终只执行一次
思路:可以通过一个变量控制 getter 的执行

代码实现
// computed.ts

class ComputedRefImpl {
  private _getter: any;
  private _dirty: boolean = true; // 控制 getter 执行
  private _value: any;
  constructor(getter) {
    this._getter = getter;
  }
  
  get value() {
    if(this._dirty) {
      this._dirty = false;
      this._value = this._getter();
    }
    
    return this._value;
  }
}

export function computed(getter) {

}
断言3

当执行到断言3会报这样一个错误

35D762E7-2A7E-4987-8961-3FD2AA34B417.png
这是因为当执行 value.foo = 2 的时候会触发 trigger,但是依赖并没有被收集起来

代码实现
// computed.ts

class ComputedRefImpl {
  private _dirty: boolean = true;
  private _value: any;
  private _effect: 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;
  }
}

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

这里有几个注意点

  • 当响应式对象的值改变, 会执行 run, 从而会执行 getter。这样就违背了 computed 懒执行的初衷
  • 当响应式对象的值改变, _dirty 需要重新变为 true 所以这里在 new ReactiveEffect 的时候,放了一个 scheduler 进去,这样当响应式对象的值改变的时候既不会执行 getter,同时也能设置 _dirty 为 true

源码地址戳这里