You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 17:37:33