5.7.3 数组的查找方法
includes/indexOf/lastIndexOf用「给定值」查找,而代理数组里存的是代理对象,直接查会查不到,必须做两处修正。
一、响应本身没问题
includes 为了找到给定值,内部会访问数组的 length 与索引,所以只要修改某个索引指向的元素值,就能触发响应:
const arr = reactive([1, 2])
effect(() => {
console.log('arr', arr.includes(1)) // 初始 true
})
arr[0] = 3 // 副作用函数重新执行,打印 false
二、问题一:同一个原始对象被重复代理
通过代理对象访问元素值时,如果这个值还能被代理,得到的会是新的代理对象而非原始对象:
const obj = {}
const arr = reactive([obj])
console.log(arr.includes(arr[0])) // false(两个代理对象并不相同)
arr[0]得到的是一次reactive产生的代理对象;includes内部通过arr访问元素,又调用了一次reactive,得到另一个代理对象;- 二者不相等 → 找不到。
解决:reactive 之前先查缓存,已经创建过就直接复用:
// 存储原始对象 → 代理对象的映射
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)) // false
原因:includes 内部的 this 指向代理对象 arr,取到的元素也都是代理对象,拿原始对象 obj 去比对自然找不到。
解决:重写数组方法 —— 先在代理对象中查找,找不到再用 this.raw 去原始数组里查一遍。为此引入 arrayInstrumentations:
// 重写 Array.prototype 上的方法
const arrayInstrumentations = {}
['includes', 'indexOf', 'lastIndexOf'].forEach(method => {
const originMethod = Array.prototype[method]
arrayInstrumentations[method] = function (...args) {
// this 是代理对象,先在代理对象中查找
let res = originMethod.apply(this, args)
// 找不到(false / -1)时,通过 this.raw 拿到原始数组再查一次
if (res === false || res === -1) {
res = originMethod.apply(this.raw, args)
}
return res
}
})
在 get 拦截器中优先返回重写后的方法:
get(target, key, receiver) {
if (key === 'raw') return target
// 目标对象是数组,且 key 在 arrayInstrumentations 上,则返回重写后的方法
if (Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
// ...track 与返回值逻辑
}
indexOf/lastIndexOf找不到时返回的是-1(不是false),所以判断条件要写成res === false || res === -1。
四、小结
| 现象 | 原因 | 处理 |
|---|---|---|
arr.includes(1) 修改元素后能触发响应 | 内部读取 length 与索引 | 不需要额外代码 |
arr.includes(arr[0]) 为 false | 同一原始对象生成了不同代理对象 | reactiveMap 缓存代理对象 |
arr.includes(obj) 为 false | 内部用代理对象比对原始对象 | arrayInstrumentations 回退到 this.raw 查找 |
相关
- 5.7.2 遍历数组:
get拦截器中的 symbol 判断 - 5.7.4 隐式修改数组长度的原型方法:
push/pop等方法为什么需要屏蔽依赖收集