使用 ES6 Proxy 实现极简响应式系统 -2: 数组变化检测的 Proxy 实现方案

问题分析

原生 Proxy 无法直接检测以下数组操作:

  • push/pop/shift/unshift

  • splice/sort/reverse

  • 直接修改数组长度 arr.length = 0

回顾之前的代码,发现虽然Proxy可以拦截数组的get和set操作,但像push、pop这些方法并不会直接触发set,因为它们是修改数组的方法,而不是通过索引赋值。例如,当使用push方法时,实际上会修改数组的length属性,并且添加新元素,但Proxy的set拦截器可能只会捕获到length的变化,而新增的元素可能不会被正确捕获,或者需要额外的处理。

接下来,需要考虑如何拦截数组的方法调用。一个常见的做法是创建一个代理数组方法的对象,覆盖原生的数组方法,这样在调用这些方法时,可以执行自定义的逻辑,比如触发回调。同时,需要保持数组其他行为的正常运作。

可能的步骤:

1. 确定需要拦截的数组方法,如push、pop、shift、unshift、splice、sort、reverse等,这些方法会修改数组本身。

2. 创建一个包含这些方法的列表,并为每个方法创建代理函数。在代理函数中,先执行原生方法,然后触发回调。

3. 在Proxy的get拦截器中,当访问数组的这些方法时,返回代理后的方法,而不是原生的方法。

4. 需要处理数组方法的参数,比如push可能会有多个参数,需要正确传递。

5. 在触发回调时,需要传递足够的信息,比如方法名称、参数、修改后的数组等,以便用户知道发生了什么变化。

6. 同时,对于通过索引直接修改数组的情况(如arr[0] = 1),Proxy的set拦截器已经可以捕获,但需要注意如果索引超出当前数组长度,此时length属性会变化,也需要触发回调。

可能的问题:

- 如何正确代理数组方法,同时不影响其他非数组对象的处理。

- 在代理方法中如何正确获取数组的原始方法,避免循环调用。

- 如何处理多个数组方法调用时的嵌套情况,比如在回调中再次修改数组,是否需要防止无限循环。

- 是否需要深度代理数组中的对象元素,比如数组中的对象属性变化是否也需要触发回调。

此外,还需要考虑代码的结构,是否将数组处理与普通对象的处理分开,或者如何在现有的Proxy handler中整合数组方法的拦截。例如,在get拦截器中,当target是数组,并且访问的属性是这些需要拦截的方法时,返回代理后的函数。否则,正常返回属性值。

举个例子,当调用arr.push(1)时,首先会通过get拦截器获取push方法,此时返回代理后的push方法。当代理后的push方法被调用时,先调用原生的push,然后触发回调,通知数组发生了变化。

另外,需要注意,直接通过索引设置数组元素(如arr[3] = 5)会被set拦截器捕获,此时需要判断设置的索引是否大于等于当前数组的length,如果是,则length属性会被改变,这时候可能需要额外的处理,或者确保set拦截器能正确处理这种情况。

测试案例方面,应该包括:

- 调用数组方法(push、pop等)是否触发回调。

- 直接通过索引修改元素是否触发回调。

- 修改数组的length属性是否触发回调。

- 嵌套数组的情况,比如数组中的元素是对象,修改这些对象的属性是否触发回调(这部分可能需要递归代理,但用户当前的问题可能仅关注数组本身的变化)

实现方案

1. 创建数组方法拦截器

const arrayMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']

const createArrayHandler = (callback) => {
  const arrayProto = Array.prototype
  const arrayHandler = Object.create(arrayProto)

  arrayMethods.forEach(method => {
    const original = arrayProto[method]
    arrayHandler[method] = function(...args) {
      const result = original.apply(this, args)
      callback({
        type: 'array',
        method,
        args,
        array: this
      })
      return result
    }
  })

  return arrayHandler
}

2. 增强 Proxy Handler

function reactive(obj, callback) {
  const arrayHandler = createArrayHandler(callback)

  return new Proxy(obj, {
    get(target, key) {
      // 处理数组方法
      if (Array.isArray(target) && arrayMethods.includes(key)) {
        return arrayHandler[key].bind(target)
      }
      
      // 处理嵌套对象
      const value = target[key]
      if (typeof value === 'object' && value !== null) {
        return reactive(value, callback)
      }
      return value
    },
    set(target, key, value) {
      // 处理数组长度变化
      if (Array.isArray(target) && key === 'length') {
        callback({
          type: 'length',
          oldValue: target.length,
          newValue: value
        })
      }
      
      const oldValue = target[key]
      target[key] = value
      
      if (oldValue !== value) {
        callback({
          type: 'set',
          targetType: Array.isArray(target) ? 'array' : 'object',
          key,
          oldValue,
          newValue: value
        })
      }
      return true
    }
  })
}

使用示例

const arr = reactive([1, 2, 3], (msg) => {
  console.log('[Array Change]', msg)
})

// 测试数组方法
arr.push(4)  // 输出 array change 日志
arr.splice(0, 1) // 触发回调

// 测试直接索引修改
arr[0] = 100 // 正常触发 set 回调

// 测试长度修改
arr.length = 0 // 触发 length 类型回调

实现效果

操作类型 是否触发回调 备注
push/pop 等方法 显示方法名称和参数
arr[index] = value 显示键值变化
arr.length = newValue 特殊 length 类型回调
sort/reverse 显示方法名称
嵌套数组操作 递归代理实现深度监听

注意事项

  1. 性能优化

 
// 缓存已代理的数组处理器
const handlerCache = new WeakMap()

function getArrayHandler(callback) {
  if (!handlerCache.has(callback)) {
    handlerCache.set(callback, createArrayHandler(callback))
  }
  return handlerCache.get(callback)
}
  1. 特殊数组操作处理

 
// 处理 concat 等非变异方法
const readOnlyMethods = ['concat', 'slice', 'map', 'filter']
readOnlyMethods.forEach(method => {
  arrayHandler[method] = function(...args) {
    return reactive(Array.prototype[method].apply(this, args))
  }
})
  1. 多维数组支持

 
// 在 set 拦截器中添加:
if (Array.isArray(value)) {
  target[key] = reactive(value, callback)
}

完整流程图解

graph TD
    A[数组操作] -->|方法调用| B{是否为变异方法?}
    B -->|是| C[代理方法处理器]
    B -->|否| D[原生方法处理]
    C --> E[执行原生方法]
    E --> F[触发回调]
    D --> G[返回正常结果]
    
    H[索引赋值] --> I[Proxy set 拦截]
    I --> J[触发属性变更回调]
    
    K[length 修改] --> L[特殊 length 处理]
    L --> M[触发 length 变更回调]

常见问题解决方案

问题1:多次触发回调

// 在回调中添加防抖处理
const createDebouncedCallback = (callback, delay = 0) => {
  let timer
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      callback(...args)
    }, delay)
  }
}

问题2:循环引用

// 使用 WeakSet 记录已代理对象
const proxySet = new WeakSet()

function reactive(obj, callback) {
  if (proxySet.has(obj)) return obj
  // ...原有逻辑...
  proxySet.add(proxy)
  return proxy
}

问题3:性能监控

// 添加性能标记
const handler = {
  set(target, key, value) {
    const start = performance.now()
    // ...原有逻辑...
    const end = performance.now()
    console.log(`SET操作耗时: ${end - start}ms`)
    return true
  }
}

通过以上方案,我们实现了对数组变化的全面检测,同时保持了 Proxy 实现的优雅性和扩展性。 

 

posted @ 2025-02-08 16:42  Yang9710  阅读(121)  评论(0)    收藏  举报