使用 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 | ✅ | 显示方法名称 |
| 嵌套数组操作 | ✅ | 递归代理实现深度监听 |
注意事项
-
性能优化
// 缓存已代理的数组处理器
const handlerCache = new WeakMap()
function getArrayHandler(callback) {
if (!handlerCache.has(callback)) {
handlerCache.set(callback, createArrayHandler(callback))
}
return handlerCache.get(callback)
}
-
特殊数组操作处理
// 处理 concat 等非变异方法
const readOnlyMethods = ['concat', 'slice', 'map', 'filter']
readOnlyMethods.forEach(method => {
arrayHandler[method] = function(...args) {
return reactive(Array.prototype[method].apply(this, args))
}
})
-
多维数组支持
// 在 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 实现的优雅性和扩展性。

浙公网安备 33010602011771号