如何在Proxy代理对象中捕获数组的增删等变更?
问题描述
我需要追踪一个被Proxy代理的对象内部数组的变更,示例对象结构如下:
const object = { count: 0, input: '', tasks: [] }
目前的问题是:Proxy可以捕获对象的set、get等操作,也能捕获数组的push、splice这类会修改原数组的操作,但无法捕获delete object.tasks[someIndex]这类删除数组元素的操作,以及array.filter((x, index) => index !== some)这类返回新数组的变更。请问正确的捕获方式是什么?
更新后的测试代码
const createElement = (any) => { const { tagName = undefined, className = '', id = undefined, state = { }, render = undefined, ...some } = any const element = tagName ? document.createElement(tagName) : document.createDocumentFragment() if(tagName){ if(className) element.className = className if(id) element.id = id } if(render){ if(state){ element.state = new Proxy(state, { deleteProperty(entry, v){ console.log(entry, v) delete(entry[v]) return true }, set(entry, k, v){ if(entry[k] !== v){ console.log(entry, v) entry[k] = v const children = render({ state: entry }) element.childNodes.forEach((child, i) => { if(typeof(children[i]) === 'string' || typeof(children[i]) === 'number') children[i] = document.createTextNode(children[i]) !children[i].isEqualNode(child) && element.replaceChild(children[i], child) && console.log('Element updated: ', children[i]) }) } else { console.log(k, v) } return true } }) } const children = render({ state: element.state }) if(typeof(children) === 'object' && children instanceof(Array)) element.replaceChildren(...children) else element.replaceChildren(children) } if(some){ Object.assign(element, some) } return element } document.querySelector('#root').replaceChildren( createElement({ tagName: 'div', className: 'class-test', render: () => ([ `buttons test (dec/inc)`, createElement({ tagName: 'div', state: { count: 0 }, render: ({ state }) => [ createElement({ tagName: 'button', className: 'btn-decrease', onclick: () => state.count--, render: () => `-` }), `Clicked ${ state.count } times...`, createElement({ tagName: 'button', className: 'btn-increase', onclick: () => state.count++, render: () => `+` }) ] }), `inputs test (tasks, click on task to remove)`, createElement({ tagName: 'form', state: { input: ``, tasks: [ ] }, onsubmit: (e) => { e.preventDefault() e.target.reset() }, render: ({ state }) => [ createElement({ tagName: `ul`, render: () => state.tasks.map((task, i) => createElement({ tagName: 'li', onclick: () => delete state.tasks[i], render: () => task })) }), createElement({ tagName: 'input', placeholder: `Text some task..`, oninput: ({ target }) => state.input = target.value }), createElement({ tagName: 'button', type: 'submit', onclick: () => state.input ? state.tasks.push(state.input) && (state.input = ``) : null, render: () => `Set task` }) ] }) ]) }) )
解决方案
问题核心是你当前只代理了外层state对象,内部的tasks数组未被Proxy包装,所以数组的delete操作、filter这类返回新数组的方法无法被外层Proxy拦截。需要做以下调整:
1. 递归代理所有嵌套对象/数组
在Proxy的get拦截器中,判断获取到的值是否为对象或数组,如果是且未被代理,就返回该值的Proxy实例,确保嵌套结构的所有操作都能被拦截。
2. 完善deleteProperty的更新逻辑
原deleteProperty仅执行删除操作,未触发视图更新,需在删除后调用render方法更新DOM。
3. 拦截数组的非修改原数组方法
像filter、map这类方法会返回新数组,不会修改原数组,需在get拦截器中包装这些方法,调用时将结果赋值回原数组,触发set拦截器的更新逻辑。
修改后的Proxy实现
const createReactiveProxy = (target, renderCallback, element) => { // 标记已代理对象,避免重复代理 if (target.__isProxy__) return target; const proxy = new Proxy(target, { get(entry, prop) { const value = entry[prop]; // 递归代理嵌套的对象或数组 if (typeof value === 'object' && value !== null) { return createReactiveProxy(value, renderCallback, element); } // 拦截数组方法,处理filter这类返回新数组的情况 if (Array.isArray(entry) && typeof value === 'function') { return function(...args) { const result = value.apply(entry, args); // 对返回新数组的方法,将结果替换回原数组 if (['filter', 'slice', 'map'].includes(prop)) { entry.length = 0; entry.push(...result); return proxy; } return result; } } return value; }, set(entry, prop, value) { if (entry[prop] !== value) { entry[prop] = value; // 触发视图更新 const children = renderCallback({ state: target }); element.childNodes.forEach((child, i) => { if (typeof children[i] === 'string' || typeof children[i] === 'number') { children[i] = document.createTextNode(children[i]); } if (!children[i].isEqualNode(child)) { element.replaceChild(children[i], child); console.log('Element updated: ', children[i]); } }); } return true; }, deleteProperty(entry, prop) { const existed = Reflect.has(entry, prop); if (existed) { delete entry[prop]; // 删除后触发视图更新 const children = renderCallback({ state: target }); element.childNodes.forEach((child, i) => { if (typeof children[i] === 'string' || typeof children[i] === 'number') { children[i] = document.createTextNode(children[i]); } if (!children[i].isEqualNode(child)) { element.replaceChild(children[i], child); console.log('Element updated: ', children[i]); } }); } return existed; } }); proxy.__isProxy__ = true; return proxy; } // 在createElement中替换原Proxy创建逻辑 if(state){ element.state = createReactiveProxy(state, render, element); }
关键改动说明
- 递归代理:确保数组和嵌套对象都被Proxy包裹,所有内部操作可被拦截。
- 数组方法拦截:包装
filter等返回新数组的方法,将结果替换回原数组,触发set拦截器的视图更新。 - deleteProperty更新:删除元素后立即调用render更新DOM,解决删除操作无响应问题。
- 避免重复代理:通过
__isProxy__标记已代理对象,防止循环代理。
测试代码调整
删除数组元素时,delete state.tasks[i]现在会触发更新;使用state.tasks = state.tasks.filter(...)这类操作也会被拦截并触发视图更新。
内容的提问来源于stack exchange,提问作者Eugene S
相关产品推荐
相关产品推荐

