Skip to content
On this page

16_实现相对完善的reactive

一、我们要优化哪些点?

既然要实现相对完善的reactive,那自然需要考虑的多一点。

那我们大概列举一些边缘case的简单考虑:

  • reactive的参数类型问题,如果传入的参数不是对象呢?
  • 如果reactive的对象已经是一个响应式对象了呢,如何处理?也就是嵌套问题:reactive(reactive(obj))
  • 那自然还有另外一个类似的问题:同一个对象,分别两次调用reactive后拿到的响应式对象还是同一个吗?

看着是不是有些头大,但是这些都是我们应该要考虑到的地方。
不着急,接下来一个个来完善。

二、reactive相关考虑完善

(一)参数类型问题

1. 单测用例
js
it('reactive params type must be object', () => {
  console.warn = jest.fn();
  // 传入的不是一个对象
  const original = reactive(1);

  expect(console.warn).toBeCalled();
  expect(original).toBe(1);
});
2. 完善逻辑

众所周知,proxy不能代理基本数据类型,所以遇到基本数据类型,我们应该直接返回原数据,并给一个提示。
那第一步,就得判断是不是对象,而且这应该是一个工具函数,所以,封装进shared

js
// src/shared/index.ts

export const isObject = (val) => {
  return val !== null && typeof val === 'object';
};

工具函数完成,那我们只需要在reactive中对入参进行判断即可。

js
// src/reactivity/reactive.ts

function createReactiveObject(target: any, baseHandlers) {
  // + 不是对象,警告,返回原数据
  if (!isObject(target)) {
    console.log(`value cannot be made reactive: ${ String(target) }`);
    return target;
  }
  return new Proxy(target, baseHandlers);
}
3. 单测结果
16_01_reactive参数类型问题单测结果

(二)多层嵌套问题

1. 单测用例
js
it('observing already observed value should return same Proxy', () => {
  const original = { foo: 1 };

  const observed = reactive(original);
  const observed2 = reactive(observed);

  expect(observed2).toBe(observed);
});
2. 完善逻辑

核心逻辑:
我们只需要判断target是否是响应式对象,是的话,则返回target,否则就按正常逻辑来。

实现思路:
仔细一看,是不是可以类比isReactiveisReadonly的实现,这样的话,那就简单了。

我们为ReactiveFlags增加一个枚举RAW,值为__v_raw
然后当嵌套时,判断target是否有ReactiveFlags[RAW]。 如果已经是响应式对象,则在createGetter中判断读取是否keyReactiveFlags[RAW],是的话,则返回target,中断reactive。 那如果不是响应式对象呢,那自然就没有这个属性,继续往下走好了。

代码实现:

ts
// src/reavtivity/reactive.ts

export const enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly',
  RAW = '__v_raw'
}

function createReactiveObject(target: any, baseHandlers, proxyMap) {
  if (!isObject(target)) {
    console.warn(`value cannot be made reactive: ${ String(target) }`);
    return target;
  }
  if (target[ReactiveFlags.RAW]) {
    return target;
  }
  return new Proxy(target, baseHandlers);
}
ts
// src/reactivity/baseHandlers.ts

function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly;
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly;
    } else if (key === ReactiveFlags.RAW) {
      // + 增加判断
      return target;
    }

    const res = Reflect.get(target, key);

    if (shallow) {
      return res;
    }

    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res);
    }

    if (!isReadonly) {
      track(target, key);
    }

    return res;
  };
}
3. 单测结果
16_02_reactive多层嵌套问题单测结果

(三)多次监测问题

1. 单测用例
js
it('observing the same value multiple times should return same Proxy', () => {
  const original = { foo: 1 };

  const observed = reactive(original);
  const observed2 = reactive(original);

  expect(observed2).toBe(observed);
});
2. 完善逻辑

当我们多次监测同一个对象时,我们拿到的应该也是同一个响应式对象。
一方面是为了性能问题,避免多次代理造成的性能浪费;
另一方面,也是为了避免一些由此引起的不必要的问题。

就举个简单的例子:

例如在依赖收集track时,我们是根据原始对象的targetkey去一一对应收集依赖进targetMap的。 此处注意是原始对象!

那么就就意味着:如果同一对象,多次代理可以返回不同的响应式对象时,那么这些响应式对象对应的依赖将会收集在同一个targetMap中。
由此带来的就是:只要其中一个响应式对象发生变化,那么所有的这些响应式对象对应的依赖都会被触发并更新,而且我们也并没有必要去增加另外的一些标识,来区分这些由同一个对象产生的不同代理,那将会增加很多心智负担,那并不是我们所期望的。

实现思路:
我们只需要将已经代理过的对象和对应的代理,按照原始对象target → 响应式对象proxy一对一存储即可。并且由于是弱引用,所以我们采用WeakMap来存储。

代码实现:

ts
// src/reactivity/reactive.ts

export const reactiveMap = new WeakMap();
export const readonlyMap = new WeakMap();
export const shallowReadonlyMap = new WeakMap();

function createReactiveObject(target: any, baseHandlers, proxyMap) {
  if (!isObject(target)) {
    console.warn(`value cannot be made reactive: ${ String(target) }`);
    return target;
  }
  if (target[ReactiveFlags.RAW]) {
    return target;
  }
  const existingProxy = proxyMap.get(target);
  // + 这里解决的是reactive多层嵌套的问题
  if (existingProxy) {
    return existingProxy;
  }
  const proxy = new Proxy(target, baseHandlers);
  // + 缓存一下已经被代理的对象
  proxyMap.set(target, proxy);
  return proxy;
}

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

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

export function shallowReadonly(target) {
  return createReactiveObject(target, shallowReadonlyHandlers, shallowReadonlyMap);
}
3. 单测结果
16_03_reactive多次监测问题单测结果