核心思路是将所有可变状态赋值收口到受控Setter中,并在Setter内部自动触发历史快照捕获,实现“写即记”。需区分可撤销/不可撤销变更、按需采集快照、支持嵌套路径,同时注意初始化、批量操作和异步更新等边界处理。

在画布应用(如低代码平台中的 Power Apps、Canvas Apps 或自定义前端画布编辑器)中,利用 Setter 属性自动记录撤销/重做历史 的核心思路是:**将所有可变状态的赋值操作统一收口到受控的 Setter 中,并在 Setter 内部触发历史快照捕获**。这避免了手动调用 pushHistory() 的遗漏和耦合,实现“写即记”,让历史栈管理真正自动化。
为什么 Setter 是自动化记录的理想入口
画布应用的状态通常分散在多个对象属性中(如 selectedItem.x、canvas.layers[0].opacity、textBlock.text)。若靠业务逻辑层逐处调用记录函数,极易漏掉某次赋值;而 Setter 天然拦截每一次属性变更,且语义明确——“这个值变了”。只要确保所有可撤销状态都通过响应式 Setter 修改,历史记录就不再依赖开发者自觉。
关键设计:带快照能力的响应式 Setter 模式
不直接使用原生 Object.defineProperty,而是封装一个支持配置化快照策略的 Setter 工厂:
-
区分“可撤销”与“不可撤销”变更:例如拖拽过程中的实时
x/y变更应节流合并为单次快照,而用户确认后的最终位置才触发记录;可通过debounce: true或batchId标识同一操作批次 -
快照内容按需采集:不是深拷贝整个对象,而是只序列化被修改属性关联的最小上下文(如仅存
{ targetId: 'btn1', property: 'width', oldValue: 120, newValue: 145 }),节省内存 -
支持嵌套路径 Setter:用
set(obj, 'layers.0.opacity', 0.7)替代obj.layers[0].opacity = 0.7,确保所有深层赋值也被拦截
实际集成步骤(以 Canvas App 自定义组件为例)
假设你有一个 Shape 类,其 left、top、fill 需支持撤销:
- 定义受控属性:
在构造时用defineReactive初始化每个可撤销字段,传入onSet: (oldVal, newVal) => recordChange(...) - 绑定画布事件:
拖拽结束时调用shape.left = newX,缩放完成时调用shape.fill = '#ff6b6b'—— 此时 Setter 自动触发快照并推入历史栈 - 撤销执行逻辑:
从栈顶取出历史项,反向调用对应 Setter(如shape.left = historyItem.oldValue),无需额外反射或 patch 引擎
注意事项与边界处理
自动化不等于无脑记录。以下情况需主动绕过 Setter 或抑制记录:
- 初始化赋值(如加载默认模板)应设
skipHistory: true标志,避免首帧污染历史栈 - 批量操作(如“全选后统一改颜色”)建议临时禁用自动记录,改为操作结束后统一提交一次快照
- 异步更新(如动画帧中连续修改
rotation)需结合requestAnimationFrame节流,在最后一帧触发记录

















