《Vue.js 设计与实现》读书笔记 - 第5章、非原始值的响应式方案

第5章、非原始值的响应式方案

5.1 理解 Proxy 和 Reflect

Proxy

  • Proxy 只能代理对象,不能代理非对象原始值,比如字符串。
  • Proxy 会拦截对对象的基本语义,并重新定义对象的基本操作。
const p = new  Proxy(obj, {
  get() {...}, // 拦截读取操作
  set() {...}, // 拦截设置操作
})

const fn = (name) => {
  console.log('i am ', name);
}

const p2 = new Proxy(fn, {
  apply(target, thisArg, argArray) {
    target.call(thisArg, ...argArray)
  }
})

p2('hcy') //  i am  hcy

Proxy 构造函数接受两个参数,第一个参数是被代理的对象,第二个参数是一个对象,包含一组夹子(trap)。

但是 Proxy 只能拦截基本操作,不能拦截复合操作。比如调用对象下的方法,obj.fn(),因为这里先通过 get 获取值再调用。

Reflect

Reflect 是一个全局对象,其下面有很多方法,而这些方法和 Proxy 拦截器都同名,作用是提供访问一个对象属性的默认行为。

image

下面这两种操作是等价的。

const obj = { foo: 1 };
console.log(obj.foo); // 直接读取
console.log(Reflect.get(obj, 'foo')); // Reflect读取

其中 Reflect.get 可以指定第三个参数,指定接受者。下面例子说明应用

点击查看代码
const obj = {
  foo: 1,
  get bar() {
    return this.foo;
  },
};
const p = new Proxy(obj, {
  get(target, key) {
    track(target, key);
    // 这里直接读取target的值
    return target[key];
  },
  set(target, key, newVal) {
    // 同样直接设置target的属性值
    target[key] = newVal;
    trigger(target, key);
    return true;
  },
});
effect(() => {
  console.log(p.bar);
});
p.foo++;

我们读取 p.bar 依赖了 foo 字段,但是修改 p.foo 的时候,却没有执行副作用函数,因为我们在读取 bar 方法中的 this.foo 读取到的是 obj 这个原始对象的属性。

通过 Reflect.get 的第三个参数指定 receiver 来解决这个问题,这里可以理解为函数的 this

const p = new Proxy(obj, {
  get(target, key, receiver) {
    track(target, key);
    // 通过Reflect.get读取值,并指定receiver
    return Reflect.get(target, key, receiver);
  },
});

5.2 JavaScript 对象及 Proxy 的工作原理

根据 ECMAScript 规范,对象分为常规对象和异质对象。

我们通过 [[xxx]] 代表对象的内部方法。比如 [[Get]],常规对象就是一些指定的内部方法按照指定定义来实现的对象,其他都是异质对象。

我们创建代理对象时,如果指定了某些拦截函数,就是指定了这个代理对象的行为,而如果没有指定,它就会调用原始对象的内部方法。

5.3 如何代理 Object

对一个普通对象的所有可能的读取操作

  • 访问属性 obj.foo
  • 判断对象或原型上是否存在给定的 key key in obj
  • 使用 for...in 循环遍历对象 for (const key in obj) {}

拦截 in 操作符:

const obj = {
  foo: 1,
};
const p = new Proxy(obj, {
  has(target, key) {
    track(target, key);
    return Reflect.has(target, key);
  },
  set(target, key, newVal) {
    target[key] = newVal;
    trigger(target, key);
    return true;
  },
});
effect(() => {
  'foo' in p;
  console.log(1111);
});
p.foo = 1

拦截 for...in 循环,通过拦截 ownKeys,因为遍历并不会操作某一个指定的属性,我们需要创建一个 key 用了记录相关依赖,然后在 ownKeys 收集依赖,在新增或删除属性时触发依赖。

const ITERATE_KEY = Symbol();
const p = new Proxy(obj, {
  ownKeys(target) {
    // 在拦截ownKeys时我们创建了一个key来收集遍历相关的依赖
    track(target, ITERATE_KEY);
    return Reflect.ownKeys(target);
  },
});

接下来要做的就是拦截新增和删除属性。就是在 trigger 函数中新增执行的遍历收集的依赖函数。直接放完整代码。

点击查看代码
let activeEffect;
const effectStack = [];
function effect(fn, options = {}) {
  const effectFn = () => {
    // 执行前先清除依赖
    cleanup(effectFn);
    // 执行前先压入栈中
    activeEffect = effectFn;
    effectStack.push(effectFn);
    const res = fn();
    // 执行后弹出
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
    // 存储fn的计算结果并返回
    return res;
  };
  // 把 options 挂在 effectFn 上
  effectFn.options = options;
  // 用来存储与该副作用函数相关联的依赖集合
  effectFn.deps = [];
  if (!options.lazy) {
    effectFn();
  }
  // 将副作用函数作为返回值返回
  return effectFn;
}
function cleanup(effectFn) {
  // 很简单 就是在每个依赖集合中把该函数删除
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  effectFn.deps.length = 0;
}
const bucket = new WeakMap();
// 在 track 中记录 deps
function track(target, key) {
  if (!activeEffect) return;
  let depsMap = bucket.get(target);
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  let deps = depsMap.get(key);
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  deps.add(activeEffect);
  // 当前副作用函数也记录下关联的依赖
  activeEffect.deps.push(deps);
}
const ITERATE_KEY = Symbol();
function trigger(target, key, type) {
  let depsMap = bucket.get(target);
  if (depsMap) {
    const effects = depsMap.get(key);
    const effectsToRun = new Set();
    effects &&
      effects.forEach((effectFn) => {
        if (effectFn !== activeEffect) {
          effectsToRun.add(effectFn);
        }
      });
    if (type === 'ADD' || type === 'DELETE') {
      const iterateEffects = depsMap.get(ITERATE_KEY);
      iterateEffects &&
        iterateEffects.forEach((effectFn) => {
          if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn);
          }
        });
    }
    effectsToRun.forEach((effectFn) => {
      // 如果一个副作用函数存在调度器 就用调度器执行副作用函数
      if (effectFn.options.scheduler) {
        effectFn.options.scheduler(effectFn);
      } else {
        // 否则就直接执行
        effectFn();
      }
    });
  }
}
const obj = {
  foo: 1,
};

const p = new Proxy(obj, {
  ownKeys(target) {
    // 在拦截ownKeys时我们创建了一个key来收集遍历相关的依赖
    track(target, ITERATE_KEY);
    return Reflect.ownKeys(target);
  },
  set(target, key, newVal, receiver) {
    const type = Object.prototype.hasOwnProperty.call(target, key)
      ? 'SET'
      : 'ADD';
    // res就是设置的结果
    const res = Reflect.set(target, key, newVal, receiver);
    trigger(target, key, type);
    return res;
  },
  deleteProperty(target, key) {
    const hadKey = Object.prototype.hasOwnProperty.call(target, key);
    const res = Reflect.deleteProperty(target, key);
    if (res && hadKey) {
      // 存在属性且删除成功才触发
      trigger(target, key, 'DELETE');
    }
    return res;
  },
});
effect(() => {
  console.log('====');
  for (const key in p) {
    console.log(key);
  }
});
p.bar = 1;
delete p.foo

5.4 合理地触发响应

赋值时要判断新旧值不相等才需要出发依赖。注意要对 NaN 特殊判断。

set(target, key, newVal, receiver) {
  const oldVal = target[key];
  const type = Object.prototype.hasOwnProperty.call(target, key)
    ? 'SET'
    : 'ADD';
  const res = Reflect.set(target, key, newVal, receiver);
  if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
    trigger(target, key, type);
  }
  return res;
},

接下来考虑对 Proxy 做一层封装,用于对任意对象做响应式封装。

function reactive(obj) {
  return new Proxy(obj, {...})
}

现在考虑这样一个场景

const obj = {};
const proto = { bar: 1 };
const child = reactive(obj);
const parent = reactive(proto);
Object.setPrototypeOf(child, parent);
effect(() => {
  console.log(child.bar);
});
child.bar = 2;

我们创建了两个响应式对象,并把 child 的原型设置为 perent,现在修改 child.bar 会发现触发了两次副作用的执行。

原因是我们的child上并没有 bar 属性,这样我们会读取到 parent 的属性,所以在 parent 上也收集了副作用函数,而设置的时候,同样会在 parent 上进行设置,这样又触发了 parent 的依赖,所以在 parentchild 上分别执行了一次。

现在的问题是,我们不应该在 parent 上进行触发。而在 set 中有第三个参数 receiver 表示设置的代理对象。

set(target, key, value, receiver) {
  // child 中
  // target 是 obj, receiver 是 child
  // parent 中
  // target 是 proto, receiver 还是 child
}

所以只需要通过 receiver 比较一下就可以了。不过我们要先拦截 get 中的 raw 属性,以便我们能够获取原始值。

get(target, key, receiver) {
  if (key === 'raw') {
    return target;
  }
  track(target, key);
  // 通过Reflect.get读取值,并指定receiver
  return Reflect.get(target, key, receiver);
},

现在我们可以通过 proxy.raw 获取代理对象的原始数据。

set(target, key, newVal, receiver) {
  const oldVal = target[key];
  const type = Object.prototype.hasOwnProperty.call(target, key)
    ? 'SET'
    : 'ADD';
  const res = Reflect.set(target, key, newVal, receiver);
  if (target === receiver.raw) {
    if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
      trigger(target, key, type);
    }
  }
  return res;
},

5.5 浅响应与深响应

我们上面实现的响应式是浅响应,如果是嵌套对象的话,修改内部的嵌套属性不会触发响应,而一般情况下我们需要实现深相应,这时我们就需要对对象递归调用 reactive

function createReactive(obj, isShallow = false) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      if (key === 'raw') {
        return target;
      }
      const res = Reflect.get(target, key, receiver);
      track(target, key);
      if (isShallow) {
        return res;
      }
      if (typeof res === 'object' && res !== null) {
        return createReactive(res);
      }
      return res;
    },
  }
}

function reactive(obj) {
  return createReactive(obj);
}
function shallowReactive(obj) {
  return createReactive(obj, true);
}

通过参数 isShallow 可以实现浅响应和深响应的切换。

5.6 只读和浅只读

有些时候我们还需要实现数据只读,这个比较简单,就是在 set 的时候和 delete 的时候拦截一下就可以,同时如果数据是只读的,也就没有比较进行响应式处理了,所以在 get 也不需要收集依赖。

点击查看代码
let activeEffect;
const effectStack = [];
function effect(fn, options = {}) {
  const effectFn = () => {
    // 执行前先清除依赖
    cleanup(effectFn);
    // 执行前先压入栈中
    activeEffect = effectFn;
    effectStack.push(effectFn);
    const res = fn();
    // 执行后弹出
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
    // 存储fn的计算结果并返回
    return res;
  };
  // 把 options 挂在 effectFn 上
  effectFn.options = options;
  // 用来存储与该副作用函数相关联的依赖集合
  effectFn.deps = [];
  if (!options.lazy) {
    effectFn();
  }
  // 将副作用函数作为返回值返回
  return effectFn;
}
function cleanup(effectFn) {
  // 很简单 就是在每个依赖集合中把该函数删除
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  effectFn.deps.length = 0;
}
const bucket = new WeakMap();
// 在 track 中记录 deps
function track(target, key) {
  if (!activeEffect) return;
  let depsMap = bucket.get(target);
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  let deps = depsMap.get(key);
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  deps.add(activeEffect);
  // 当前副作用函数也记录下关联的依赖
  activeEffect.deps.push(deps);
}
const ITERATE_KEY = Symbol();
function trigger(target, key, type) {
  let depsMap = bucket.get(target);
  if (depsMap) {
    const effects = depsMap.get(key);
    const effectsToRun = new Set();
    effects &&
      effects.forEach((effectFn) => {
        if (effectFn !== activeEffect) {
          effectsToRun.add(effectFn);
        }
      });
    if (type === 'ADD' || type === 'DELETE') {
      const iterateEffects = depsMap.get(ITERATE_KEY);
      iterateEffects &&
        iterateEffects.forEach((effectFn) => {
          if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn);
          }
        });
    }
    effectsToRun.forEach((effectFn) => {
      // 如果一个副作用函数存在调度器 就用调度器执行副作用函数
      if (effectFn.options.scheduler) {
        effectFn.options.scheduler(effectFn);
      } else {
        // 否则就直接执行
        effectFn();
      }
    });
  }
}

function reactive(obj) {
  return new Proxy(obj, {
    ownKeys(target) {
      // 在拦截ownKeys时我们创建了一个key来收集遍历相关的依赖
      track(target, ITERATE_KEY);
      return Reflect.ownKeys(target);
    },
    get(target, key, receiver) {
      if (key === 'raw') {
        return target;
      }
      track(target, key);
      // 通过Reflect.get读取值,并指定receiver
      return Reflect.get(target, key, receiver);
    },
    set(target, key, newVal, receiver) {
      const oldVal = target[key];
      const type = Object.prototype.hasOwnProperty.call(target, key)
        ? 'SET'
        : 'ADD';
      const res = Reflect.set(target, key, newVal, receiver);
      if (target === receiver.raw) {
        if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
          trigger(target, key, type);
        }
      }
      return res;
    },
    deleteProperty(target, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, key);
      const res = Reflect.deleteProperty(target, key);
      if (res && hadKey) {
        // 存在属性且删除成功才触发
        trigger(target, key, 'DELETE');
      }
      return res;
    },
  });
}

function createReactive(obj, isShallow = false, isReadonly = false) {
  return new Proxy(obj, {
    ownKeys(target) {
      // 在拦截ownKeys时我们创建了一个key来收集遍历相关的依赖
      track(target, ITERATE_KEY);
      return Reflect.ownKeys(target);
    },
    get(target, key, receiver) {
      if (key === 'raw') {
        return target;
      }
      if (!isReadonly) {
        track(target, key);
      }
      const res = Reflect.get(target, key, receiver);
      
      if (isShallow) {
        return res;
      }
      if (typeof res === 'object' && res !== null) {
        return isReadonly ? readonly(res) : reactive(res);
      }
      return res;
    },
    set(target, key, newVal, receiver) {
      if (isReadonly) {
        console.warn(`属性 ${key} 是只读的.`);
        return true;
      }
      const oldVal = target[key];
      const type = Object.prototype.hasOwnProperty.call(target, key)
        ? 'SET'
        : 'ADD';
      const res = Reflect.set(target, key, newVal, receiver);
      if (target === receiver.raw) {
        if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
          trigger(target, key, type);
        }
      }
      return res;
    },
    deleteProperty(target, key) {
      if (isReadonly) {
        console.warn(`属性 ${key} 是只读的.`);
        return true;
      }
      const hadKey = Object.prototype.hasOwnProperty.call(target, key);
      const res = Reflect.deleteProperty(target, key);
      if (res && hadKey) {
        // 存在属性且删除成功才触发
        trigger(target, key, 'DELETE');
      }
      return res;
    },
  });
}

function reactive(obj) {
  return createReactive(obj);
}
function shallowReactive(obj) {
  return createReactive(obj, true);
}
function readonly(obj) {
  return createReactive(obj, false, true);
}
function shallowReadonly(obj) {
  return createReactive(obj, true, true);
}

const obj = reactive({ foo: { bar: 1 } });
effect(() => {
  console.log(obj.foo.bar);
});
obj.foo.bar = 2;

5.7 代理数组

数组是异质对象,所以有些操作需要特殊处理。

5.7.1 数组索引与 length

我们通过下标设置或获取元素值,比如arr[0]=1 都可以正常通过拦截。但是数组中还有个特殊的属性 length,我们设置的下标如果大于 lengthlength 会被更新。我们设置 length 如果小于之前的 length,那么大于 length 的元素都会被删除,我们要把对应的副作用函数全部执行。

这里主要是调整 settrigger

点击查看代码
function trigger(target, key, type, newVal) {
  let depsMap = bucket.get(target);
  if (depsMap) {
    const effects = depsMap.get(key);
    const effectsToRun = new Set();
    effects &&
      effects.forEach((effectFn) => {
        if (effectFn !== activeEffect) {
          effectsToRun.add(effectFn);
        }
      });
    if (type === 'ADD' || type === 'DELETE') {
      const iterateEffects = depsMap.get(ITERATE_KEY);
      iterateEffects &&
        iterateEffects.forEach((effectFn) => {
          if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn);
          }
        });
    }
    if (type === 'ADD' && Array.isArray(target)) {
      const lengthEffects = depsMap.get('length');
      lengthEffects &&
        lengthEffects.forEach((effectFn) => {
          if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn);
          }
        });
    }
    if (Array.isArray(target) && key === 'length') {
      depsMap.forEach((effects, key) => {
        if (key >= newVal) {
          effects.forEach((effectFn) => {
            if (effectFn !== activeEffect) {
              effectsToRun.add(effectFn);
            }
          });
        }
      });
    }
    effectsToRun.forEach((effectFn) => {
      // 如果一个副作用函数存在调度器 就用调度器执行副作用函数
      if (effectFn.options.scheduler) {
        effectFn.options.scheduler(effectFn);
      } else {
        // 否则就直接执行
        effectFn();
      }
    });
  }
}

function createReactive(obj, isShallow = false, isReadonly = false) {
  return new Proxy(obj, {
    set(target, key, newVal, receiver) {
      if (isReadonly) {
        console.warn(`属性 ${key} 是只读的.`);
        return true;
      }
      const oldVal = target[key];
      const type = Array.isArray(target)
        ? Number(key) < target.length
          ? 'SET'
          : 'ADD'
        : Object.prototype.hasOwnProperty.call(target, key)
        ? 'SET'
        : 'ADD';
      const res = Reflect.set(target, key, newVal, receiver);
      if (target === receiver.raw) {
        if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
          trigger(target, key, type, newVal);
        }
      }
      return res;
    },
  });
}

const arr = reactive([1, 2]);
effect(() => {
  console.log(arr[0]);
  console.log(arr[1]);
});
arr.length = 1; // 1 undefined

5.7.2 遍历数组

我们之前通过 for...in 遍历数组,自定义了一个 ITERATE_KEY,但是对于数组来说,我们只需要通过 length 收集依赖就可以了。

    ownKeys(target) {
      // 在拦截ownKeys时我们创建了一个key来收集遍历相关的依赖
      track(target, Array(target) ? 'length' : ITERATE_KEY);
      return Reflect.ownKeys(target);
    },

对于 for...of 遍历我们不需要特殊处理,因为我们对元素和length都做了处理。当然我们知道迭代器是通过 @@iterator(Symbol.iterator) 指定的,而为了避免错误和性能考虑,我们不应该和 symbol 值建立响应关系。

    get(target, key, receiver) {
      if (key === 'raw') {
        return target;
      }
      if (!isReadonly && typeof key !== 'symbol') { // 新增
        track(target, key);
      }
      // ...
    },

5.7.3 数组的查找方法

const obj = {}
const arr = reactive([obj])
console.log(arr.includes(arr[0]));

按照之前的代码,上面的情况会返回 false 因为我们每次 get 的时候,如果获取的是对象就会进行响应式操作,这样导致两次读取的时候,生成了两次 proxy 对象,所以不相等。现在我们需要维护一个映射,对于同一个原始对象应该返回相同的 proxy 对象。

// 存储原始对象到代理的映射
const reactiveMap = new Map();
function reactive(obj) {
  const existionProxy = reactiveMap.get(obj);
  if (existionProxy) return existionProxy;
  const proxy = createReactive(obj);
  reactiveMap.set(obj, proxy);
  return proxy;
}

但是很显然还是有问题,我们把数组元素对象变成响应式的代理值了,那么我们再查原始值明显差不到了。

const obj = {}
const arr = reactive([obj])
console.log(arr.includes(obj));

解决思路就是,把代理值匹配一遍,再把原始值匹配一遍。

const arrayInstrumentations = {};
['includes', 'indexOf', 'lastIndexOf'].forEach((method) => {
  const originMethod = Array.prototype[method];
  arrayInstrumentations[method] = function (...args) {
    let res = originMethod.apply(this, args);
    if (res === false) {
      res = originMethod.apply(this.raw, args);
    }
    return res;
  };
});

    get(target, key, receiver) {
      if (key === 'raw') {
        return target;
      }
      if (Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)) {
        return Reflect.get(arrayInstrumentations, key, receiver);
      }
      // ...
    }

5.7.4 隐式修改数组长度的原型方法

下面的代码会造成死循环,因为两个副作用函数都会监听 length 但是又都会修改 length

const arr = reactive([]);
effect(() => {
  arr.push(1);
});
effect(() => {
  arr.push(1);
});

修改思路就是屏蔽对 length 的监听。引因为 push 的语义是修改而不是读取,我们可以屏蔽原始操作的响应。

let shouldTrack = true;
const arrayInstrumentations = {};
['push', 'pop', 'shift', 'unshift', 'splice'].forEach((method) => {
  const originMethod = Array.prototype[method];
  arrayInstrumentations[method] = function (...args) {
    shouldTrack = false;
    let res = originMethod.apply(this, args);
    shouldTrack = true;
    return res;
  };
});

function track(target, key) {
  if (!activeEffect || !shouldTrack) return;
  // ...
}

5.8 代理 Map 和 Set

这些数据结构和普通对象相比有很多特殊的属性和方法,所以需要特殊处理。

5.8.1 如何代理 Map 和 Set

const s = new Set([1, 2, 3]);
const p = new Proxy(s, {})
console.log(p.size);

如上程序会出现报错:"TypeError: Method get Set.prototype.size called on incompatible receiver #"

实际上 Set.prototype.size 是一个属性访问器,它的 set访问器为 undefined,它的 get访问器计算 set 的元素个数并返回。

get 访问器内部会调用内部方法,但是现在我们通过代理调用的时候,由于我们把 this 指定为 proxy 对象,所以报错了,所以我们需要改动之前的代码,如果是原始对象是 Set 且获取 size 的时候,我们把 receiver 指定为原始对象。

const s = new Set([1, 2, 3]);
const p = new Proxy(s, {
  get(target, key, receiver) {
    if (key === 'size') {
      return Reflect.get(target, key, target)
    }
    return Reflect.get(target, key, receiver)
  }
})
console.log(s.size);

同时 delete 也会出现类似问题,不过 delete 是函数而不是访问器,所以我们可以通过 bind 来指定 this。最后需要把这部分集成到之前的代码。

  get(target, key, receiver) {
    if (key === 'size') {
      return Reflect.get(target, key, target)
    }
    return target[key].bind(target)
  }

其次就是对 size 进行响应式处理,在 adddelete 时触发,和前面对普通对象的处理一样,绑定到 ITERATE_KEY 键上,并在 adddelete 时触发。

5.8.3 避免污染原始数据

这里是说如果在 Map 中设置一个响应式数据的话,会导致用户通过原始值调用也会触发响应式操作导致混乱。所以我们设置值的时候,如果发现是响应式值,只保存它的原始值。

这里使用的 raw 可能与用户定义属性重名,可以使用 Symbol 避免,同时,其他集合类型,Set 和数组都需要做类似的处理。

点击查看代码
  set(key, value) {
    const target = this.raw;
    const had = target.has(key);
    const oldValue = target.get(key);
    // 获取原始值并设置
    const rawValue = value.raw || value;
    target.set(key, rawValue)
    if (!had) {
      trigger(target, key, 'ADD');
    } else if (
      oldValue !== value &&
      (oldValue === oldValue || value === value)
    ) {
      trigger(target, key, 'SET');
    }
  },

5.8.4 处理 forEach

forEach 遍历 Map 时,我们要对 ITERATE_KEY 建立响应,同时不仅在 adddelete 时触发,在 set 时也要触发相应,因为遍历时要读取值。

同时,我们在获取值的时候,应该做响应式处理。

// trigger 函数中如果对象是 Map,那么 SET 也出要出发相应
if (
      type === 'ADD' ||
      type === 'DELETE' ||
      (type === 'SET' &&
        Object.prototype.toString.call(target) === '[object Map]')
    )

然后 mutableInstrumentations 添加 forEach 函数

  forEach(callback, thisArg) {
    const wrap = (val) => (typeof val === 'object' ? reactive(val) : val);
    const target = this.raw;
    track(target, ITERATE_KEY);
    target.forEach((v, k) => {
      callback.call(thisArg, wrap(v), wrap(k), this);
    });
  },

5.8.5 迭代器方法

集合类型有三个迭代器方法:entries,keys,values。其中,

m[Symbol.iterator] === m.entries // true

我们实现这个方法,第一点注意一定要有 Symbol.iterator 属性,其次要把key/value 都做响应式处理,然后要和 ITERATE_KEY 绑定,

点击查看代码
const mutableInstrumentations = {
  // ...
  [Symbol.iterator]: iterationMethod,
  enties: iterationMethod,
};

function iterationMethod() {
  const target = this.raw;
  const itr = target[Symbol.iterator]();
  const wrap = (val) => (typeof val === 'object' ? reactive(val) : val);
  track(target, ITERATE_KEY);

  return {
    next() {
      const { value, done } = itr.next();
      return {
        value: value ? [wrap(value[0]), wrap(value[1])] : value,
        done,
      };
    },
    // 实现可迭代协议
    [Symbol.iterator]() {
      return this;
    },
  };
}

5.8.6 values 和 keys 方法

方法和 entries 类似。不过要注意,对于 entriesvalues 即使是 SET 操作也需要触发执行,但是 keys 只有在新增和删除才会执行,所以给它单独绑定到 MAP_KET_ITERATE_KEY

代码懒得写了。。。。

posted @ 2023-01-30 17:52  我不吃饼干呀  阅读(60)  评论(0编辑  收藏  举报