之前我们说到,数组的方法内部其实都依赖了对象的基本语义,因此多数情况下,不需要特殊处理就能让方法按预期执行。就比如数组的includes方法,看下面代码:
const arr = reactive([1,2])
effect(()=>{
console.log(arr.includes(1)) // 初始打印 true
})
arr[0] = 3 // 副作用函数重新执行,并打印false
这是因为includes方法为了找到给定的值,内部会访问数组的length属性以及数组的索引,因此修改某个索引值后能够触发响应。
但是includes方法并不总是按照预期工作,如下:
const obj = {}
const arr = reactive([obj])
console.log(arr.includes(arr[0])) // false
这里预期的结果是true,但是返回的却是false
这里我们通过查阅语言规范,了解includes方法的执行流程是怎样的。
通过阅读语言规范得知,arr.includes(arr[0])这段代码中,arr[0]得到的是一个代理对象,而在includes方法内部也会通过arr访问数组元素,从而得到一个代理对象,而问题在于这两个代理对象是不同的,这是因为每次调用reactive函数时都会创建一个新的代理对象。
function reactive(obj){
// 即使obj相同,还是会创建新的代理对象
return createReactive(obj)
}
这个问题的解决方案如下:
// 定义一个Map实例,存储原始对象到代理对象的映射
const reactiveMap = new Map()
function reactive(obj){
// 先通过原始对象obj寻找之前创建的代理对象,如果找到了直接返回已有的代理对象
const existionProxy = reactiveMap.get(obj)
if(existionProxy) return existionProxy
// 如果没找到,则创建新的代理对象
const proxy = createReactive(obj)
// 存储到Map中,帮助重复创建
reactiveMap.set(obj, proxy)
return proxy
}
这样就避免了为同一个原始对象多次创建代理对象的问题。接下来再运行开头的代码就能得到预期的结果了
const obj = {}
const arr = reactive([obj])
console.log(arr.includes(arr[0])) // true
但是看下面的代码
const obj = {}
const arr = reactive([obj])
console.log(arr.includes(obj)) // false
这样直接把原始对象作为参数传递给includes,可是在数组中仍然找不到obj对象。其实原因很简单,因为includes内部的this指向的是代理对象arr, 并且在获取数组元素时得到的值也是代理对象,所以拿原始对象obj去查找肯定找不到。因此需要重写数组的includes方法并实现自定义的行为,才能解决这个问题。首先看下如何重现includes方法,如下面代码:
const arrayInstrumentations = {
includes: function(){/*...*/}
}
function createReactive(obj, isShallow = false, isReadonly = false){
return new Proxy(obj, {
get(target, key, receiver){
if(key === 'raw'){
return target
}
// 如果操作的目标对象时数组,并且key存在于arrayInstrumentations上
// 那么返回定义在arrayInstrumentations上的值
if(Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)){
return Reflect.get(arrayInstrumentations, key, receiver)
}
if(!isReadonly && typeof key !== 'symbol'){
track(target, key)
}
const res = Reflect.get(target, key, receiver)
if(!isShallow){
return res
}
if(typeof res === 'object' && res !== null){
return isReadonly?readonly(res):reactice(res)
}
return res
}
})
}
当执行arr.includes时,实际执行的是定义在arrayInstrumentations上的includes函数,这样就实现了重写
接下来来自定义includes函数:
const originMethod = Array.prototype.includes
const arrayInstrumentations = {
includes: function(...args){
// this是代理对象,现在代理对象中查找,将结果存储到res中
let res = originMethod.apply(this, args)
if(res === false){
// res为false说明没有找到,通过this.raw拿到原始数组,再去其中查找并更新res值
res = originMethod.apply(this.raw, args)
}
return res
}
}
这样再运行下面的代码就能按预期执行了
const obj = {}
const arr = reactive([obj])
console.log(arr.includes(obj)) // true
除了includes方法外,还需要做类似处理的数组方法有indexOf和lastIndexOf,因为这些方法都是属于根据指定的值返回查找结果的方法,完整代码如下:
const arrayInstrumentations = {}
;['includes','indexOf','lastIndexOf'].forEach(method => {
// this是代理对象,现在代理对象中查找,将结果存储到res中
let res = originMethod.apply(this, args)
if(res === false){
// res为false说明没有找到,通过this.raw拿到原始数组,再去其中查找并更新res值
res = originMethod.apply(this.raw, args)
}
return res
})
知识扩展
apply方法的作用
其作用有两个,跟它的入参有关。
- 改变this指向。
- 将数组入参变为一般入参。
详细可参考:https://blog.csdn.net/weixin_43877799/article/details/120282509