0%

代理数组

对数组元素或者属性的读取操作:

通过索引访问数组元素值:arr[0]

访问数组的长度:arr.length

把数组作为对象,使用for…in循环遍历

使用for…of迭代遍历数组

数组的原型方法,如concat/join/every/some/find/findIndex/includes等,以及不改变原数组的原型方法

1 数组索引与length

通过索引设置数组元素的值时,会执行内部方法[[Set]],内部方法[[Set]]依赖于[[DefineOwnProperty]],当设置的索引值大于数组当前长度,更新数组length属性,触发与length属性相关联的副作用函数重新执行,修改set拦截函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
set(target,key,newValue){
//如果是只读的打印警告信息
if(isReadOnly){
console.warn(`属性${key}是只读的`)
}
//获取旧值
const oldValue=target[key]
//如果代理目标对象是数组,则检测被设置的索引值是否小于数组长度
//如果是,则为SET操作,否则为ADD操作
const type=Array.isArray(target)
?Number(key)<length?'SET':'ADD'
:Object.prototype.hasOwnProperty.call(target,key)?'SET':'ADD'

//设置属性值
const res=Reflect.set(target,key,receiver,newValue)
//说明receiver是target的代理对象
if(target===receiver.raw){
//比较新值和旧值,只有当它们不全等并且都不是NAN才触发响应
if(oldValue!==newValue&&(oldValue===oldValue||newValue===newValue)){
//假如设置数组length属性为0,会影响数组元素,因此要触发新的响应
trigger(target,key,type,newValue)
}
}

return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function trigger(target,key){
const depsMap=bucket.get(target)
if(!depsMap)return
const effects=depsMap.get(key)
const effectsToRun=new Set(effects)
//当操作类型是ADD或者DELETE,需要触发与length相关的副作用函数执行
if(type==='ADD'||type==='DELETE'){
const iterateEffects=depsMap.get('length')
lengthEffects&&lengthEffects.forEach(effectfn=>{
if(effectfn!==activeEffect){
effectsToRun.add(effectfn)
}
})
}
effects&&effects.forEach(effectfn=>{
if(activeEffect!=effectfn){//只有当trigger触发执行的副作用函数和当前正在执行的副作用函数不相同时才触发执行,否则会出现栈溢出
effectsToRun.add(effectfn)
}
})
effectsToRun.forEach(effectfn=>effectfn())
//effect&&effect.forEach(fn=>fn())//会产生无限执行
}

2 数组查找方法

arr.includes(arr[0])中arr是代理对象,includes函数执行时this指向的是代理对象,即arr,includes方法会通过索引读取数组元素值,如果值时可以被代理的,那么得到的值就是新的代理对象,

1
2
3
4
5
function reactive(obj){
//每次调用reactive时都会创建新的代理对象
return createReactive(obj)

}

解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
//当参数Obj是相同的不用创建新的代理对象
//存储原始对象到代理对象的映射
const reactiveMap=new Map()
function reactive(obj){
//优先通过原始对象obj查找之前创建的代理对象,如果找到了,直接返回已有的代理对象
const existionProxy=reactiveMap.get(obj)
if(existionProxy)return existionProxy
//否则创建新的代理对象
proxy=createReactive(obj)
reactiveMap.set(obj,proxy)
return proxy

}

然而,下面这段代码

1
2
3
const obj = {};
const arr = reactive([obj])
console.log(arr.includes[obj])//false

includes内部的this指向的是代理对象arr,并且在获取数组元素时得到的也是代理对象,所以用原始对象obj去查找找不到,返回false,因此我们需要重写includes方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const arrInstrumentations={}
//重写方法
['includes','indexOf','lastIndexOf'].forEach(method=>{
const originMethod=Array.prototype[method]
arrInstrumentations[method]=function(...args){
//先在代理对象中查找,结果存储在res实现了arr.includes(obj)的默认方法
//找不到就去原始数组上查找
const res=originMethod.apply(this,args)
//找不到则在原始对象中查找
if(res===false){
res=originMethod.apply(this.raw,args)
}
}
return res;
})
1
2
3
4
5
6
7
8
9
10
11
12
get(target,key,receiver){
//通过"raw”属性访问原始对象
if(key==='raw'){
return target
}
//如果操作对象存在于arrInstrumentations上,返回定义在arrInstrumentation上的值
if(Array.isArray(target)&&arrInstrumentations.hasOwnProperty(key)){
return Reflect.get(arrInstrumentations,key,receiver)
}
...
}

3 push/pop/shift/unshift等方法

当调用数组的push方法时,即会读取数组length属性值也会设置数组length属性值,会导致两个独立的副作用函数相互影响,就像

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

会得到栈溢出的错误

分析:

  • 第一个副作用函数执行,在该函数内,调用arr.push方法向数组中添加一个元素,调用数组push方法时会间接读取数组的length属性,所以第一个副作用函数执行完毕会与length属性建立响应联系
  • 第二个副作用函数执行,同样,与length属性建立响应联系,同时调用arr.push会设置length属性,于是响应式系统尝试把与length有关的副作用函数全部取出执行,就包括第一个副作用函数,此时,第二个副作用函数还未执行完毕就去调用第一个副作用函数
  • 第一个副作用函数再次执行,也会间接设置数组的length属性,于是响应系统又尝试把所以与length属性相关联娿副作用取出执行,其中包括第二个副作用函数
  • 循环往复导致栈溢出

因此,我们可以通过屏蔽对length属性的读取,避免在它与副作用函数之间建立联系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//一个标记变量,代表是否追踪
let shouldTrack = true
['push'].forEach(method=>{
//取得原始push方法
const originMethod = Array.prototype[method]
//重写
arrInstrumentations[method] = function(...args){
//在调用方法前禁止追踪
shouldTrack=false;
let res = originMethod.apply(this,args)
//调用原始方法后,恢复原来行为,即允许追踪
shouldTrack=true
return res
}
})

在执行默认行为之前先将shouldTrack置false,禁止追踪,当push方法默认行为执行完毕后,将shouldTrack还原为true,

1
2
3
4
5
function track(target,key){
//禁止追踪时直接返回
if(!activeEffect || !shouldTrack)return
...
}

当push方法间接读取length属性,由于此时是禁止追踪状态,所以length属性与副作用函数之间不会建立响应联系,也就不会产生栈溢出