5.7.4 隐式修改数组长度的原型方法
push/pop/shift/unshift/splice内部会读取并设置length,会让副作用函数相互触发直到栈溢出,必须屏蔽这类「读取」。
一、问题复现
const arr = reactive([])
effect(() => { arr.push(1) })
effect(() => { arr.push(1) }) // 栈溢出:Maximum call stack size exceeded
二、成因
以 push 为例(规范 23.1.3.20 节),它既读取也设置数组的 length:
- 第一个副作用函数执行,
push间接读取length→ 与该length建立响应联系; - 第二个副作用函数执行,同样与
length建立联系,而push又间接设置length; - 设置
length触发与length关联所有副作用函数,其中包含第一个副作用函数 → 它再次执行并再次设置length; - 如此往复,最终调用栈溢出。
三、解决:用 shouldTrack 屏蔽追踪
问题的关键是 push 间接读取了 length。只要在调用原型方法期间「禁止追踪」,就不会建立响应联系:
let shouldTrack = true // 默认允许追踪
function track(target, key) {
// 禁止追踪时直接返回
if (!activeEffect || !shouldTrack) return
// ...
}
重写这些方法,在调用原始方法前后切换开关:
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(method => {
const originMethod = Array.prototype[method]
arrayInstrumentations[method] = function (...args) {
shouldTrack = false // 调用原始方法之前,禁止追踪
const res = originMethod.apply(this, args)
shouldTrack = true // 调用之后恢复追踪
return res
}
})
这类方法在语义上都是修改操作而非读取操作,所以屏蔽期间的读取不会造成信息丢失,也不会带来其他副作用。
四、小结
| 方法 | 为什么需要处理 |
|---|---|
push / unshift | 添加元素时读取并设置 length |
pop / shift | 删除元素时读取并设置 length |
splice | 增删元素时隐式修改 length |
相关
- 5.7.3 数组的查找方法:
arrayInstrumentations的引入方式 - 5.8 代理 Set 和 Map:换到集合类型,内部槽会带来新的问题