使用 ES6 Proxy 实现极简响应式系统 -3: 数组非变异方法处理

在处理 concatslicemapfilter 等数组方法时需要特殊处理,是因为这些方法与 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[返回代理后的响应式数组]

五、设计必要性总结

  1. 保持响应式连续性
    确保链式操作中所有中间结果都具备响应式能力,避免出现响应式断层。

  2. 符合开发者直觉
    使用者无需关心返回的是否为代理对象,所有数组操作表现一致。

  3. 完整的数据追踪
    避免以下情况导致响应丢失:

     
    const filtered = reactiveArr.filter(x => x > 2)
    filtered.push(3) // 需要被监听到
  4. 性能优化平衡
    仅在方法调用时动态创建代理,避免预先代理所有可能产生的数组。

posted @ 2025-02-08 17:43  Yang9710  阅读(27)  评论(0)    收藏  举报