5.8.4 处理forEach
forEach的遍历既关心键也关心值,因此「值变了」这种SET操作也必须触发响应,且回调参数要包装成响应式。
一、实现:与 ITERATE_KEY 建立联系
- 遍历操作只与键值对的数量有关,任何修改数量的操作(
add/delete等)都应触发副作用函数重新执行; - 所以
forEach被调用时,让副作用函数与ITERATE_KEY建立响应联系。
forEach(callback, thisArg) {
// wrap 把可代理的值转换为响应式数据
const wrap = (val) => (typeof val === 'object' ? reactive(val) : val)
const target = this.raw
// 与 ITERATE_KEY 建立响应联系
track(target, ITERATE_KEY)
// 通过原始对象调用原生 forEach
target.forEach((v, k) => {
// 手动调用 callback,用 wrap 包裹 key / value 后再传入,实现深响应
callback.call(thisArg, wrap(v), wrap(k), this)
})
}
二、缺陷一:回调参数不是响应式数据
因为内部是通过原始数据对象调用原生 forEach,传给 callback 的参数也是原始数据:
const p = reactive(new Set([1, 2, 3, { foo: 1 }]))
effect(() => {
p.forEach((value) => {
// value 是原始对象时,访问 size 无法建立响应联系
console.log(value.size)
})
})
这不符合直觉:reactive 本身是深响应,forEach 回调拿到的参数也应该是响应式数据。
解决:用 wrap 把参数包装成响应式数据(见上面的实现)。
三、缺陷二:SET 操作也要触发
对比两种遍历:
| 遍历方式 | 关心的是 | SET(改值)是否需要触发 |
|---|---|---|
for...in 遍历普通对象 | 只关心键 | 不需要(键的数量没变) |
forEach 遍历 Map | 既关心键,又关心值 | 需要 |
所以 trigger 里触发 ITERATE_KEY 依赖的条件要放宽:
if (
type === 'ADD' || type === 'DELETE' ||
// 操作类型是 SET 且目标对象是 Map 时,也要触发与 ITERATE_KEY 相关联的副作用函数
(type === 'SET' && target instanceof Map)
) {
const iterateEffects = depsMap.get(ITERATE_KEY)
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) effectsToRun.add(effectFn)
})
}
const p = reactive(new Map([['key', 1]]))
effect(() => {
p.forEach((value) => {
console.log(value) // 1
})
})
p.set('key', 2) // 虽然是 SET,仍然要触发响应
出于严谨性,
forEach的第二个参数thisArg也要透传给callback:callback.call(thisArg, wrap(v), wrap(k), this)。
相关
- 5.8.3 避免污染原始数据:Map 的
set为什么需要区分 ADD / SET - 5.8.5 迭代器方法:
for...of迭代集合的代理方式