闭包节流与命令模式协同实现画布撤销:节流控制操作频率,仅在mouseup等终态触发命令创建,聚合连续操作;闭包命令封装最小必要状态快照,确保undo可靠;命令管理器统一调度历史栈、限制深度并兼容canvas特性。

闭包节流与命令模式协同实现画布撤销,核心在于:用节流控制操作频率,用闭包命令封装可逆动作,两者配合避免高频绘制导致历史栈爆炸或状态错乱。
节流层负责“收口”,防止无效命令堆积
画布拖拽、连续笔触等操作极易在毫秒级内触发数十次状态变更。若每次都生成完整命令,history 数组会迅速膨胀,内存飙升且 undo 效率骤降。节流在此处不是为了 UI 流畅,而是为历史管理“守门”。
- 只对有意义的终态做快照:比如鼠标抬起(
mouseup)、触摸结束(touchend)或连续绘制间隔 > 100ms 时,才触发命令创建 - 节流函数不丢弃操作,而是聚合:把一段连续的点序列合并为单个
DrawCommand,而非为每个mousemove创建一条命令 - 示例节流封装:
const throttleCommand = (fn, delay = 100) => {
let timeout = null;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
};
// 绑定到 canvas 的 drawEnd 事件
canvas.addEventListener('mouseup', throttleCommand(() => {
const cmd = createDrawCommand(strokesBuffer);
commandManager.execute(cmd);
}, 80));
闭包命令负责“自带快照”,确保 undo 可靠
每个命令对象必须在创建时捕获执行前的最小必要状态,而不是依赖外部变量或运行时读取——这是闭包的价值所在。它让命令自包含、无副作用、可重复调用 undo。
- 快照内容按操作类型精简:移动图形只需记录
x/y;填充颜色只需存fillStyle;路径绘制则保存toDataUrl()或差分路径数据 - 避免 DOM 查询陷阱:不要在
undo()中重新查canvas.getContext('2d'),而应在闭包内固化上下文引用或关键属性 - 典型闭包命令结构:
const createMoveCommand = (shape) => {
const prevX = shape.x;
const prevY = shape.y;
return {
execute: (newX, newY) => { shape.x = newX; shape.y = newY; },
undo: () => { shape.x = prevX; shape.y = prevY; }
};
};
命令管理器统一调度,处理分支与边界
节流后的命令进入管理器,它不只 push/pop,还要维护当前索引、清理重做栈、限制最大深度,并兼容 Canvas 特性(如无法擦除单条线时,用快照回退)。
- 新命令执行前,清空
redoStack,并截断history中当前索引之后的所有项 - 支持两种撤销策略混合:普通操作走命令 undo;Canvas 全量绘制操作可 fallback 到
getImageData()快照(需节流后才快照,否则太重) - 最大历史数建议设为 50–100,超出时
shift()而非pop(),保证老操作先淘汰
协同与持久化需额外约定,但不破坏本地逻辑
若画布支持多人协作或刷新保留,节流+闭包命令结构依然成立,只需在命令对象中增加元信息字段,并由外部同步机制接管广播与冲突处理。
- 每个命令自动携带
timestamp和userId,便于服务端排序与过滤 - 本地
undo()仍调用闭包内逻辑;服务端下发的undo消息也解析为同构命令对象执行 - 页面刷新时,将当前
history序列化存入localStorage,恢复时重建闭包命令(注意函数不可序列化,需用工厂函数 + 状态数据还原)
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











