富文本编辑器的撤销功能依赖命令模式+快照机制,而非单纯回退状态;需即时捕获光标、选区等动态参数并序列化存储,再通过apply重放还原。

在富文本编辑器中实现撤销(undo)功能时,关键不是单纯“回退状态”,而是精准还原执行命令时的全部动态参数——比如光标位置、选区范围、插入内容、样式属性等。而 apply 本身是 JavaScript 中用于调用函数并传入参数数组的方法,它不直接管理历史或状态,但可作为“命令重放”的核心执行机制。真正起作用的是命令模式(Command Pattern) + 命令快照(Command Snapshot)+ 参数序列化与反序列化。
用命令对象封装可撤销操作及其动态参数
每次用户触发格式化、插入、删除等操作,不直接修改 DOM,而是生成一个命令对象,包含:
-
执行方法(如
insertHtml、setBold) -
动态参数(如
{ html: '<strong>test</strong>', range: { start: 12, end: 17 } }) -
反向操作方法(或逆命令)(如
deleteRange或保存前一 DOM 片段)
这些参数必须在命令创建时即时捕获(例如用 getSelection() 获取当前 range,用 innerHTML 快照节点内容),而非延迟读取。
历史栈中存储参数快照,而非引用
撤销栈(如 undoStack = [])应 push 命令对象的深拷贝(尤其含 range、node、selection 等易失效对象):
- 对
Range或Selection,不能存原对象(下次撤销时已失效),需序列化为{ anchorNode: nodePath, anchorOffset: x, focusNode: ..., focusOffset: y },配合 DOM 路径定位算法还原 - 对动态生成的内容(如时间戳、随机 ID、用户输入实时值),必须在命令创建时立即捕获并固化
- 推荐使用
structuredClone(现代浏览器)或轻量 JSON 序列化 + 自定义还原逻辑(如 range → path + offset)
用 apply 批量恢复参数并重放命令
当执行撤销时,从栈顶取出命令对象,用 apply 调用其执行函数,并传入还原后的参数数组:
const cmd = undoStack.pop(); const args = restoreArgs(cmd.serializedArgs); // 如把 path 还原为真实 node,offset 定位 range cmd.execute.apply(editorContext, args); // ✅ 此处 apply 实现「参数动态恢复后批量调用」
注意:apply 的作用是确保函数以原始意图被调用——比如 document.execCommand 已废弃,但自定义 formatText('bold') 若需接收多个上下文参数(节点、range、value),apply 就能干净地展开数组传参。
避免常见陷阱:range 失效、异步延迟、DOM 变更干扰
即使参数恢复了,若 DOM 已被其他操作修改,还原可能错位。需:
- 所有编辑操作(包括撤销/重做)都走同一套命令管道,保证原子性
- 撤销前先冻结编辑器交互(禁用输入、防并发),还原后再恢复
- 对 range 类型参数,采用“延迟定位”策略:不立刻还原,而在
execute内部用findNodeByPath()+range.setStart/End动态重建 - 必要时在命令中嵌入 DOM 版本号或 content-hash,撤销前校验是否仍匹配当前编辑器状态
本质上,apply 是工具,不是方案。真正让撤销“参数动态恢复”可靠的,是命令设计的完备性、参数捕获的即时性、以及还原逻辑的健壮性。











