proxy本身不处理异步操作,但能精准捕获同步写入行为;多级撤销依赖将异步流程建模为事务提交,通过set拦截记录变更、snapshot生成快照、栈管理支持路径感知的分组回退。

Proxy 本身不直接处理异步操作,但它能精准捕获所有同步的数据写入行为——而“多级撤销”的核心,恰恰在于把每一次有意义的状态变更(无论是否由异步逻辑触发)封装为可追溯、可重放的原子操作。关键不是让 Proxy 等待异步完成,而是让异步流程主动向 Proxy 驱动的状态系统提交变更指令。
用 Proxy 构建可快照的响应式基座
先创建一个支持深度响应式的代理对象,它在 set 拦截中自动记录变更,并暴露当前状态快照接口:
- 使用 Reflect.set 执行真实赋值,保证语义不变
- 每次 set 触发时,调用 recordMutation 记录:时间戳、路径(如
['user', 'profile', 'name'])、旧值、新值 - 提供 snapshot() 方法,返回结构化快照对象(不含函数、不深拷贝引用,仅保留可序列化的变更上下文)
- 避免在 Proxy 中做深克隆,改用 structuredClone 或轻量级差分快照策略,降低开销
将异步操作转化为可回溯的“事务提交”
撤销能力不依赖操作是同步还是异步,而依赖它是否被显式建模为一次“事务”。例如一个异步保存用户头像的流程:
FastAPI + Flask 混合部署最佳实践,解决路由定义、API 代理等常见问题,适用于同时运行 FastAPI API 与 Flask 前端的场景。
- 用户点击上传 → 触发
startUpload() - 上传成功回调中,不再直接修改 state,而是调用
commit({ type: 'UPDATE_AVATAR', payload: url }) - commit 函数内部:先 snapshot 当前状态,再通过 Proxy 更新数据,最后将该次 commit 推入 undo stack
- 失败时调用
rollback(),从栈顶弹出并恢复上一快照,而非尝试“取消网络请求”
实现多级撤销的栈管理与路径感知
单纯用数组存快照容易误撤无关变更。应按“变更粒度”分级管理:
- 底层:每个 set 拦截生成细粒度变更项(字段级),用于精确还原
- 中层:业务层调用
beginGroup('edit-profile')和endGroup(),自动聚合连续变更,形成逻辑组(如一次表单编辑包含 5 个字段更新,只算 1 步可撤销) - 顶层:支持
undo(3)回退最近 3 个逻辑组,或undoTo('save-draft-20260430')跳转到带标签的节点 - 撤销时,逐项应用快照中的旧值,跳过已被后续变更覆盖的路径(用 WeakMap 缓存路径最新版本号)
与框架协同:不侵入,只衔接
这套模型不绑定任何 UI 框架,但可通过简单适配接入:
- React 中,用 useReducer 管理 undo/redo 栈,reducer 的 action 类型映射到 commit 类型
- Vue 中,将 Proxy 实例作为 reactive 的 target,配合 watch 监听变更事件来触发快照记录
- 跨组件共享时,把带撤销能力的 Proxy 实例注入 context 或 pinia store,各组件只读取和 commit,不直接修改
- 注意:异步操作的 loading 状态、错误信息等辅助字段,应单独维护,不纳入撤销栈,避免干扰主数据流










