跳到主要内容

撤销重做文档模型

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 引用

权威资料

核对日期:2026-08-26