Vue3 reactive api 处理数组

1,167 阅读3分钟

数组

  1. key(索引) < target.length -> 操作类型为 SET, 正常通过索引修改属性值,和操作对象一样,无须特别处理
const arr = reactive([0])

effect(() => {
    console.log(arr[0], 'effects run')
})

arr[0] = 1 // 触发响应
  1. 通过索引影响数组长度,key(索引) >= target.length -> 操作类型为 ADD 改变了数组的长度, trigger 需要对 length 属性进行特别处理
const arr = reactive([0])

effect(() => {
    console.log(arr.length, 'effect run') // effect 访问 length 属性
})
arr[1] = 1 // 隐式修改了length,触发响应

// set时 判断操作类型, 当key(索引) < 数组长度时,操作类型为 SET 和第一种情况一样,
// 当 key(索引) >= 数组长度,操作类型为ADD,需要特别处理
const type = key < target.length ? 'SET' : 'ADD' //

// trigger 处理 需要手动获取 length的依赖然后触发
if (type === 'ADD' && Array.isArray(target)) {
    const lengthEffects = depsMap.get('length')
    lengthEffects &&
        lengthEffects.forEach((effect) => {
            if (effect !== 'activeEffect') {
                effectsToRun.add(effect)
            }
        })
}
  1. 通过 修改 length 属性影响数组元素,数组元素索引 >= 新的 length, 需要特别处理, 数组元素索引 < 不需要处理

    const arr = reactive([0])
    arr.length = 100 // 不会影响 使用了arr[0]的地方,不需要特别处理
    
    arr.length = 0 // 间接影响了使用arr[0] 的地方,需要特别处理
    
    const arr = reactive([0])
    
    effect(() => {
        console.log(arr[0], 'effects run')
    })
    
    arr.length = 0 // 需要触发响应
    
    // 源码 实现
    if (Array.isArray(target) && key === 'length') {
        // 需要对 depsMap 遍历,而不是 effects,切记
        // depsMap: (0: effects, 1: effects) 遍历的是 depsMap
        depsMap.forEach((effects, key) => {
            if (key >= newVal) {
                // key 是索引,索引 >= 新的length 的 effects 都要重新执行
                effects.forEach((effect) => {
                    if (effect !== activeEffect) {
                        effectsToRun.add(effect)
                    }
                })
            }
        })
    }
    
  2. 对数组 for in, 本质上就是监听数组 length 属性的变化,在 ownKeys 里面使用 length 作为 key 去 track

// 添加新元素 影响for in
// 修改数组长度 影响 for in
const arr = reactive([0])
effect(() => {
    for (let index in arr) {
    }
    console.log('effect run')
})
arr[1] = 1 // 触发响应式
arr.length = 2 // 触发响应式

// 源码实现
  ownKeys(target) {
   // 如果是数组,使用track时的key为length,对象是自定义的 ITERATE_KEY
   track(target, Array.isArray(target) ? 'length' : ITERATE_KEY)
   return Reflect.ownKeys(target)
  }
  1. for of , 会读取数组的 index 属性 && length 属性
const arr = reactive([1, 2])

effect(() => {
    for (let value of arr) {
    }
    console.log('effect run')
})
arr[1] = 2 // 触发响应,不用加额外的处理,for of 时数组的每个索引都添加了依赖
arr.length = 0 // 触发响应,不用加额外处理,for of 读取了length属性,length也保存了依赖25
  1. proxyArr.includes(proxyArr[0]) 拿到的是 false

    const obj = {}
    const proxyArr = reactive([obj])
    proxyArr.includes(proxyArr[0]) // false
    
    // includes() 底层是通过索引访问对象,因为 proxyArr是一个reactive的对象,proxyArr[0]是对象,访问这个属性,会把这个属性值变为响应式的对象,相当于 obj = reactive(obj)
    // proxyArr[0] 自己访问时,也是因为相同的元素,也会再次创建一个  reactive(obj)
    // 两个不同的reactive对象,导致includes() 是false
    
    // 解决方法 reactive设置缓存对同一个对象只创建一个reactive对象
    const reactiveMap = new Map()
    function reactive(obj) {
        // 一个对象已经是 reactive的,不会再次调用reactive 方法
        const existionProxy = reactiveMap.get(target)
        if (existionProxy) return existionProxy
        const proxy = createReactive(obj)
        reactiveMap.set(obj, proxy)
        return proxy
    }
    
  2. proxyArr.includes(origin) 拿到的也是 false

    数组 includes, lastIndexOf,indexOf 底层都会通过索引去访问数组的每个成员

const obj = {}
const proxyArr = reactive([obj])
proxyArr.includes(obj) // falss 同样的代理,底层includes()拿到的是proxy对象,proxy对象 !== obj, 所以返回false


// 解决方法,拦截 includes | lastIndexOf | indexOf 三个方法,

// 拦截器,拦截数组 includes lastIndexOf,indexOf 三个方法
const arrayInstrumentations = {}
;['includes', 'indexOf', 'lastIndexOf'].forEach((method) => {
    const originMethod = Array.prototype[method] // 原方法
    arrayInstrumentations[method] = function (...args) {
        // 通过 Reflect.get(arrayInstrumetations, key, reciver) 来获取的,这个函数的this 指向 proxy
        let res = originMethod.apply(this, args) // 当this为proxy 对象时,如果获取到的结果为false,需要再次使用原对象去获取结果

        if (res === false) {
            res = originMethod.apply(this.raw, args) // 原对象
        }
        return res
    }
})
get(target,key,receiver) {
    // 省略部分代码

    // 拦截 includes | lastIndexOf | indexOf 三个方法,外部是通过 proxyArr.includes 调用的
    if (Array.isArray(target) && arrayInstrumentations[key]) {
        return Reflect.get(arrayInstrumentations,key,reciver)
    }
}
  1. ['push', 'pop', 'shift', 'unshift', 'splice'] 五个方法 即会读取 length 属性,也会设置 length 属性,导致 track & trigger 一起触发,隐式修改数组长度,并且读取 length 属性的等等方法 ['push', 'pop', 'shift', 'unshift', 'splice']

    解决方法:push等等方法底层 读取 length 时,将 shouldTrack 设为 false,不允许 track

// 第一个effect track & trigger 正常操作
// 第二个effect 执行过程中,会trigger,导致第一个effect也执行,第一个effect也会trigger,导致第二个effect又执行,又导致第一个effect执行,造成栈溢出
effect(() => {
    arr.push(1)
})

effect(() => {
    arr.push(1)
})

let shouldTrack = true
function enableTracking() {
    shouldTrack = true
}
function pauseTracking() {
    shouldTrack = false
}
;[
    // 拦截push 方法
    'push',
].forEach((method) => {
    const originMethod = Array.prototype[method]
    arrayInstrumentations[method] = function (...args) {
        pauseTracking() // 将 shouldTrack设为false
        let res = originMethod.apply(this, args)
        enableTracking() // 执行完之后 还原shouldTrack
        return res
    }
})

// 源码实现

function track(target, key) {
    if (!activeEffect || !shouldTrack) return
}