对数组元素或者属性的读取操作:
通过索引访问数组元素值: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] 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) if(target===receiver.raw){ if(oldValue!==newValue&&(oldValue===oldValue||newValue===newValue)){ 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) 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){ effectsToRun.add(effectfn) } }) effectsToRun.forEach(effectfn=>effectfn()) }
|
2 数组查找方法
arr.includes(arr[0])中arr是代理对象,includes函数执行时this指向的是代理对象,即arr,includes方法会通过索引读取数组元素值,如果值时可以被代理的,那么得到的值就是新的代理对象,
1 2 3 4 5
| function reactive(obj){ return createReactive(obj) }
|
解决方法:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
const reactiveMap=new Map() function reactive(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])
|
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){ 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){ if(key==='raw'){ return target } 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=>{ 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属性与副作用函数之间不会建立响应联系,也就不会产生栈溢出