大型html编辑器撤销恢复必须用状态机解耦操作意图与回滚契约,因structuredclone无法克隆dom节点(含事件、光标、选区等隐式状态),存在兼容性差、内存爆炸、状态丢失等问题;应采用命令对象封装execute/undo/redo,并以索引式状态机替代双栈管理。

大型 HTML 编辑器的撤销恢复不能靠堆快照硬扛,必须用状态机解耦操作意图、执行路径与回滚契约——否则栈爆炸、光标丢失、中文输入错位、内存飙升全是必然结果。
为什么不能直接用 structuredClone 存整个编辑器 DOM
看似省事,实则埋雷:DOM 节点含隐式引用(事件监听器、input 光标、contenteditable 选区),深拷贝后这些状态全丢;structuredClone 在 Safari 16.4+ 才支持,旧版需 fallback 到手动提取关键字段;单次快照体积轻易破 MB,50 步就吃掉百兆内存。真实项目里,你存的不是 DOM,是「可逆性契约」。
命令对象必须携带 execute、undo、redo 三方法
每个用户操作(插入节点、删除段落、加粗文本)都应封装为独立命令类,而非只记 HTML 字符串。这样能精确控制还原粒度,避免副作用。
-
execute()负责将变更应用到当前编辑器状态,并触发视图更新 -
undo()不是重载 innerHTML,而是调用parentNode.insertBefore(node, ref)或range.deleteContents()等原生 DOM 操作逆向还原 -
redo()必须是undo()的严格对称操作,不能复用execute()—— 否则在多次 undo/redo 切换时会累积偏差 - 命令构造时需捕获当时选区:
window.getSelection().getRangeAt(0)序列化为{ startContainer, startOffset, endContainer, endOffset }
状态机核心:用 position 索引替代栈 pop/push
传统双栈(undoStack/redoStack)在高频操作下易失序;改用带索引的状态机更可控:
- 维护一个扁平数组
states: Array<editorstate></editorstate>和当前指针position: number - 每次新操作:截断
states从position + 1开始的部分,push 新状态,position++ -
undo():仅position--,再用states[position]同步内容与选区 -
redo():仅position++,同上 - 该模式天然防栈污染,且支持跳转任意历史版本(如“回到 3 分钟前”)
中文输入、composition、MutationObserver 怎么协同
纯 input 事件漏中文输入法上屏瞬间;纯 keydown 抓不到粘贴和格式化操作;必须分层响应:
- 监听
compositionstart→ 暂停节流存档,标记「输入法中」 - 监听
compositionend→ 触发一次强制存档,确保上屏内容被捕获 - 用
MutationObserver监听编辑器根节点的childList和characterData变更,捕获粘贴、拖拽、execCommand(若仍存在)等绕过事件流的操作 - 所有存档入口统一走节流函数(如
setTimeout(..., 300)),合并连续变更,避免每键一存
真正难的不是写 undo 函数,而是让每一次 undo() 都像没动过一样——光标在原位、滚动条没跳、iframe 里的选区也还原、shadow DOM 下的 getSelection() 不报错。这些细节不显眼,但用户一碰就感知得到。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











