Skip to content

数组中属性的响应式

JavaScript中数组是一个特殊的对象。

ts
const array = reactive(['a','b','c','d'])

effect(() => {
    console.log(array[0])
})

setTimeout(() => {
    array[0] = 'e'
}, 1000)

数组也是能正确监听响应结果的,因为索引也是数组的属性。但是数组还有其他属性,比如length

ts
const array = reactive(['a','b','c','d'])

effect(() => {
    console.log(array.length)
})

setTimeout(() => {
    array.length = 2
}, 1000)

但是改变了length之后,数组就变成了['a','b'],这就意味着删除了cd,那么如果之前访问了c,在修改length,会通知吗

ts
const array = reactive(['a','b','c','d'])

effect(() => {
    console.log(array[2]) // c
})

setTimeout(() => {
    array.length = 2 // 一秒钟没触发 effect
}, 1000)

没有触发effect,就意味着需要手动触发依赖的effect重新执行了。现在修改length变成2,就是说之前收了所有>=length的依赖,都要触发更新。

ts
export trigger(target, key) {
    // 先看一下之前有没收集过这个对象的依赖
    let depsMap = targetMap.get(target)
    if (!depsMap) {
        // 没有收集过,直接返回
        return
    }

    const targetIsArray = Array.isArray(target)
    const newLength = target.length
    // 此处处理修改 lenght 导致的副作用
    if (targetIsArray && key === 'length') {
        /**
         * 一开始:['a', 'b', 'c', 'd'] length = 4
         * 更新后:['a', 'b'] length = 2
         */
        depsMap.forEach((dep, depKey) => {
        if (depKey === 'length' || depKey >= newLength) {
            // 通知更新
            propagate(dep.subs)
        }
        })
    } else {
        // 看一下之前有没有收集过这个 key
        let dep = depsMap.get(key)

        if (!dep) {
        //  如果这个 key 没收集过,直接返回
        return
        }

        // 通知更新
        propagate(dep.subs)
    }
}

当修改length之后,手动触发所有>=length的依赖更新,就可以了。

隐式更新length

什么叫隐式更新呢?比如pushpop这些方法,都会隐式更新length,他们在执行期间,会更新length。一旦有一个操作,触发了 length 的更新,那必然在 set 之前和 set 之后,它的 length 是不一样的,那么我们就可以利用这一点来选择对数组的 length 进行一个手动更新:

ts
// reactivity/src/baseHandler.ts

export const mutableHandlers = {
    set(target, key, newValue, receiver) {
        // 看一下 target 是不是一个数组
        const targetIsArray = Array.isArray(target)

        // set 前,拿到老的 length
        const oldLength = targetIsArray ? target.length : 0

        // 省略了一些不相关的代码

        const res = Reflect.set(target, key, newValue, receiver)
        // set 后拿到最新的 length
        const newLength = targetIsArray ? target.length : 0

        if (targetIsArray && newLength !== oldLength && key !== 'length') {
        /**
         * 如果更新之前和更新之后,length 不一样,代表隐式更新了,手动触发
         */
        trigger(target, 'length')
        }

        return res
    }
}

Released under the MIT License.