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
}
}