撤销重做文档模型
SVG 没有 undo()。历史记录的是 文档操作(插节点、改属性、改树顺序),不是像素快照。把每次画面 toDataURL 压栈:内存按步数 × 分辨率爆炸,而且和相机缠死。Canvas 那篇谈位图层何时不得不 snapshot;这边默认 没有 位图层。
1. 命令
export interface Command {
readonly label: string;
apply(): void;
revert(): void;
}
export class History {
private undoStack: Command[] = [];
private redoStack: Command[] = [];
private coalescing: Command | undefined;
execute(cmd: Command): void {
cmd.apply();
this.undoStack.push(cmd);
this.redoStack.length = 0;
}
/** 拖手柄期间把多次 translate 合成一条 */
coalesce(cmd: Command): void {
this.coalescing?.revert();
cmd.apply();
this.coalescing = cmd;
}
commitCoalesce(): void {
if (!this.coalescing) return;
this.undoStack.push(this.coalescing);
this.coalescing = undefined;
this.redoStack.length = 0;
}
undo(): void {
const cmd = this.undoStack.pop();
if (!cmd) return;
cmd.revert();
this.redoStack.push(cmd);
}
redo(): void {
const cmd = this.redoStack.pop();
if (!cmd) return;
cmd.apply();
this.undoStack.push(cmd);
}
}
export class SetAttrCommand implements Command {
readonly label = "attr";
private previous: string | null;
constructor(
private readonly el: Element,
private readonly name: string,
private readonly next: string,
) {
this.previous = el.getAttribute(name);
}
apply(): void {
this.el.setAttribute(this.name, this.next);
}
revert(): void {
if (this.previous === null) this.el.removeAttribute(this.name);
else this.el.setAttribute(this.name, this.previous);
}
}
生产不要让命令直接认 DOM。命令认文档对象(nodeId + 字段),apply 改文档并标记 dirty,投影层再写 SVG。否则 chrome 节点、虚拟化卸载、协作回放会对不上。
拖节点:pointerdown 记下矩阵,move coalesce(new SetTransform(id, next)),up commitCoalesce()。撤销一步回到拖之前,不是几十步。
2. 什么时候才碰位图
| 操作 | 历史 |
|---|---|
| 加/删/改矢量节点 | 命令 |
| 改 viewBox 相机 | 通常 不进 undo;或单独「视图」栈 |
| 提交一笔墨迹 | 命令(path 对象) |
| 像素橡皮、滤镜烘焙 | 该资源 old/new ImageBitmap 引用,仍包在命令里 |
相机进 undo 会让用户「撤业务」时画面乱跳。分开绑快捷键。
上限:50 步;丢栈底并释放位图资源。不要按「整页 PNG」做差分。
3. 和协作
本地 undo 不能无脑改已广播的操作。OT/CRDT 下撤销是「再发一条逆操作」,不是弹本地栈。单机编辑器和多人白板不要共用一个 History 当同步源。
打开用户文件失败时不要把半棵消毒树当一步可撤——解析要么提交完整 ReplaceDoc,要么不进栈。
4. 失败形态
| 症状 | 原因 |
|---|---|
| 撤销一次选框还在 | chrome 和文档混在一棵树上一起改了 |
| 拖一下 undo 50 次 | 没 coalesce |
| 内存涨到几百 MB | 每步整页 PNG / getImageData |
| redo 丢了 | execute 时没清 redo |
| 协作对方乱跳 | 把本地 history 当了文档 |
| 虚拟化后 undo 崩 | 命令持有已 remove 的 Element 引用 |
权威资料
- 无 SVG 专用 undo 规范。命令式编辑器模式;DOM 突变见 DOM Living Standard。
- 对比像素层:Canvas 撤销重做
核对日期:2026-08-26