『手写Vue3』Readonly 和优化 Stop

106 阅读3分钟

前情提要:上一期我们实现了基本的Effect和Reactive。

  • Effect的作用是立即执行回调函数,并且设置activeEffect。
  • Reactive类在getter中实现了依赖收集,将activeEffect加入dep;在setter中实现了依赖触发,遍历dep中的每一个effect,并执行run/scheduler方法。
  • Effect函数需要返回runner,runner即是ReactiveEffect.run方法,执行runner能拿到回调函数的返回值。
  • Effect函数可以传入一个options对象,其中包括Scheduler和onStop,它们都是回调函数。Scheduler的作用是延迟回调被执行的时间。
  • Stop函数需要传入runner作为参数,在runner中保存effect属性,在effect中保存deps属性,然后将effect从每个dep中移除。Stop之后,即使修改了响应式对象的属性,effect也不会被触发。

这一次我们来实现Readonly,isReactive/isReadonly 以及优化Stop。

readonly,isReative / isReadonly

和reactive类似,都需要使用proxy,不同之处在于,readonly的属性只能访问不能修改。因此在实现上,getter中不做依赖收集,setter执行会抛出警告。

describe('readonly', () => {
  it('happy path', () => {
    const user = { age: 10 };
    const readonlyUser = readonly(user);
    expect(readonlyUser).not.toBe(user);
    expect(readonlyUser.age).toBe(10);

    expect(isReadonly(user)).toBe(false);
    expect(isReadonly(readonlyUser)).toBe(true);
  });

  it('should call console.warn when set', () => {
    console.warn = jest.fn();
    const user = { age: 10 };
    const readonlyUser = readonly(user);

    readonlyUser.age++;
    // 产生一次警告
    expect(console.warn).toHaveBeenCalled();
  });
});

此外,对代码稍加优化,把向proxy传入getter和setter的操作封装了起来,把new Proxy()封装成语义化更好的createReactiveObject函数。

isReactive/isReadonly实现的逻辑,就是去访问特定的属性,然后在getter中将该操作拦截下来,得到一个返回值。如果是原始对象,不会进入getter,那么会得到undefined,需要用!!转为false。

enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly'
}

export function reactive(raw) {
  return createReactiveObject(raw, mutableHandlers);
}

export function readonly(raw) {
  return createReactiveObject(raw, readonlyHandlers);
}

function createReactiveObject(target, baseHandlers) {
  return new Proxy(target, baseHandlers);
}

export function isReactive(value) {
  // 当传入原始对象时,直接访问该属性会得到undefined,需转换成false
  return !!value[ReactiveFlags.IS_REACTIVE];
}

export function isReadonly(value) {
  return !!value[ReactiveFlags.IS_READONLY];
}

const get = createGetter();
const set = createSetter();
const readonlyGet = createGetter(true);

function createGetter(isReadonly = false) {
  return function (target, key) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly;
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly;
    }
    // 依赖收集
    if (!isReadonly) {
      track(target, key);
    }
    return Reflect.get(target, key);
  };
}

function createSetter() {
  return function (target, key, value) {
    // 依赖触发
    const res = Reflect.set(target, key, value);
    trigger(target, key);
    return res;
  };
}

const mutableHandlers = {
  get,
  set
};

const readonlyHandlers = {
  get: readonlyGet,
  set: (target, _key, _value) => {
    console.warn('warn: set attribute on readonly object', target);
    return true;
  }
};

优化Stop

之前Stop其实还存在bug,如果执行obj.prop = 3,dummy的值确实还是2。如果obj.prop++,dummy的值就会变成3,为什么会这样呢?

  it('stop', () => {
    let dummy;
    const obj = reactive({ prop: 1 });
    const runner = effect(() => {
      dummy = obj.prop;
      return 1;
    });

    obj.prop = 2;
    expect(dummy).toBe(2);
    stop(runner);
    
    // obj.prop = 3
    // 换成++以后,就不能通过了
    obj.prop++;
    expect(dummy).toBe(2);

    // stopped effect should still be manually callable
    const result = runner(); // 确保执行runner不会收集依赖
    obj.prop++;
    expect(result).toBe(1);
    expect(dummy).toBe(3); // dummy的值不应该变成4
  });

因为++操作的本质是先get,再set,即obj.prop = obj.prop + 1,在get中又执行了track,把effect添加进了dep中,set的时候又触发到该effect。

所以我们有如下几个目标:

  • 使得默认情况下getter不收集依赖,只有当运行effect.run()时才track。
  • 如果已经stop过,肯定无法通过setter -> trigger去执行回调了,但是可以拿到runner,通过runner()的方式,依然能够执行回调,而runner会调用effect.run(),可能引发track。所以run方法中需要添加一些逻辑,当检测到已stop时,避免track。

设置全局变量shouldTrack

通过active属性判断是否已经stop,一般情况下在执行回调之前,先将shouldTrack设置为true,从而让回调在执行的过程中能收集依赖。如果已经stop,则直接执行回调(上一次run的最后已经将shouldTrack设置为false),这样runner()就不会触发依赖收集了。

  run() {
    // 通过
    if (!this.active) {
      return this._fn();
    }

    activeEffect = this;
    // 在执行回调前,先设置shouldTrack
    // 确保本次回调执行的过程中,如果访问了响应式对象的属性,依然能够track
    shouldTrack = true;
    const res = this._fn();
    // 回调执行完毕,重置shouldTrack,使得默认情况下getter不收集依赖
    shouldTrack = false;

    return res;
  }

getter依然会触发,但是可能不会track:

function isTracking() {
  return shouldTrack && activeEffect !== undefined;
}

export function track(target, key) {
  if (!isTracking()) return;

  // ...
}