使用 ES6 Proxy 实现极简响应式系统 -3: 数组非变异方法处理
在处理 concat、slice、map、filter 等数组方法时需要特殊处理,是因为这些方法与 push/pop 等方法的本质行为不同。以下是具体原因和实现差异的分析:
一、两类方法的本质区别
| 变异方法(push/pop 等) | 非变异方法(concat/slice 等) | |
|---|---|---|
| 是否修改原数组 | ✅ 直接修改原数组 | ❌ 返回新数组,不修改原数组 |
| 响应式需求 | 需要触发回调 | 需要确保返回的新数组也具备响应式能力 |
| 典型方法 | push/pop/shift/splice 等 | concat/slice/map/filter 等 |
二、为何需要特殊处理
1. 响应式链式调用需求
当开发者执行以下代码时:
const newArr = reactiveArray.concat([4])
newArr.push(5) // 期望这里也能触发响应
如果不对 concat 做特殊处理:
-
newArr会是普通数组,无法触发响应式回调 -
push(5)操作不会被监听
2. Proxy 拦截的局限性
-
原生方法直接调用:
concat等方法直接调用Array.prototype的原生方法 -
返回值未代理:原生方法返回的是普通数组,不具备响应式能力
三、两种 Handler 的差异对比
// 原 createArrayHandler 的变异方法处理
arrayMethods.forEach(method => {
arrayHandler[method] = function(...args) {
const result = original.apply(this, args)
callback() // 触发回调
return result
}
})
// 特殊处理的非变异方法
readOnlyMethods.forEach(method => {
arrayHandler[method] = function(...args) {
return reactive( // 关键差异点:返回代理后的新数组
Array.prototype[method].apply(this, args)
)
}
})
| 特性 | 变异方法处理器 | 非变异方法处理器 |
|---|---|---|
| 是否修改原数组 | ✅ | ❌ |
| 主要目的 | 监听数据变化并触发回调 | 保持响应式链式调用能力 |
| 返回值处理 | 直接返回原生方法结果 | 将返回的新数组包装为响应式对象 |
| 是否需要触发回调 | ✅ 立即触发 | ❌ 不触发(但需保证后续操作的响应式能力) |
| 典型场景 | arr.push() 直接修改数据 |
arr.map() 产生新数据集 |
四、实现原理图解
graph TD
A[调用数组方法] --> B{是否为变异方法?}
B -->|是| C[执行原生方法 + 触发回调]
B -->|否| D[执行原生方法 + 包装返回值]
C --> E[返回原生结果]
D --> F[返回代理后的响应式数组]
五、设计必要性总结
-
保持响应式连续性
确保链式操作中所有中间结果都具备响应式能力,避免出现响应式断层。 -
符合开发者直觉
使用者无需关心返回的是否为代理对象,所有数组操作表现一致。 -
完整的数据追踪
避免以下情况导致响应丢失:const filtered = reactiveArr.filter(x => x > 2) filtered.push(3) // 需要被监听到 -
性能优化平衡
仅在方法调用时动态创建代理,避免预先代理所有可能产生的数组。

浙公网安备 33010602011771号