2025.09.23

响应式系统中的数组长度联动

在之前的处理中,虽然对对象的处理已经完善,但数组与对象不同

在之前的处理中,虽然对对象的处理已经完善,但数组与对象不同

数组是一个“特殊客户”。数组与普通对象最大的区别在于:数组的索引(index)与 length 属性之间存在强联动关系。

手动修改数组长度

如果通过 length 删除了索引, 那依赖索引的 effect 要从新触发

  • 场景 A
    const state = reactive(['a', 'b', 'c', 'd'])
    
    effect(() => {
      console.log(state[3]) // d
    })
    
    // ✅ 修改成功,触发更新
    setTimeout(() => {
      state[3] = 'e'
    })
    
  • 场景 B
effect(() => {
  console.log(state[2]) // c
})

// 修改 length 导致 state[2] 被删除
setTimeout(() => {
  state.length = 2
}, 1000)

虽然 length 更改触发了更新,但对于已经被收集依赖的 state[2], 因为已经被删除了, 所以这个effect 后续不会再发生任何行为的 set 导致没有机会被重新触发, 所以丢失了依赖关系

解决方案

length 缩短的同时, 找到被删除元素索引的 effect, 并通知他们重新执行

// dep.ts
export function trigger(target, key) {
  const depsMap = targetMap.get(targrt)
  if (!depsMap)
    return

  const targetIsArray = Array.isArray(target)
  // 情况 1. 如果是数组且修改了 length
  if (targetIsArray && key === 'length') {
    /**
     * 通过 length 显示更新 length
     *  更新前的数组: length = 4 =>['a', 'b', 'c', 'd']
     *  更新后的数组: length = 2 =>['a', 'b']
     *  所以结论是: 要通知访问 大于等于 length 的索引
     */
    const length = target.length
    depsMap.forEach((dep, depKey) => {
      /**
       * 1. 通知访问 大于等于 length 的 effect 重新执行
       * 2.  如果访问的是 'length' 属性的话也需要重新执行
       */
      if (depKey >= length || depKey === 'length') {
        propagate(dep.subs)
      }
    })
  }
  else {
    const dep = depsMap.get(key)
    if (dep) {
      propagate(dep.subs)
    }
  }
}

处理隐式更新

除了手动给 length 赋值, 通过 push 等方法也会改变长度, 这是就需要拦截数组的操作

  • 场景 C
const array = reactive(['a', 'b', 'c', 'd'])

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

setTimeout(() => {
  array.push('e')
}, 1000)

解决方案

set 拦截器中, 需要对比 修改前后的长度。 如果长度发生了变化, 即使操作的是某次的索引(arr4 = 'e'), 也要主动触发一次 length 的依赖更新

export const mutableHandlers: ProxyHandler<any> = {
  set(target, key, newValue, receiver) {
    const oldValue = target[key]
    const targetIsArray = Array.isArray(target)

    const oldLength = targetIsArray ? target.length : 0

    const res = Reflect.set(target, key, newValue, receiver)

    if (isRef(oldValue) && !isRef(newValue)) {
      oldValue.value = newValue
      return res
    }

    if (hasChanged(newValue, oldValue)) {
      trigger(target, key)
    }

    const newLength = targetIsArray ? target.length : 0
    if (targetIsArray && oldLength !== newLength && key !== 'length') {
      /**
       * 隐式更新 length
       *  更新前的数组: length = 4 => ['a', 'b', 'c', 'd']
       *  更新后的数组: length = 2 => ['a', 'b', 'c', 'd', 'e']
       *   push shift unshift splice 都会隐式更新 length
       */
      trigger(target, 'length')
    }
    return res
  }
}