应弃用浏览器原生撤销栈和document.execcommand,改用状态快照+栈管理:监听input/compositionend/mouseup事件,防抖保存innerhtml与序列化选区,还原时重建光标位置,并禁用原生ctrl+z/y。

浏览器对 contenteditable 元素的原生撤销栈(Ctrl+Z/Ctrl+Y)只记录用户直接触发的操作,JS 修改如 element.innerHTML = 或 textContent 完全不进栈——想靠它做可靠 undo/redo,必踩坑。
为什么 document.execCommand('undo') 不该用
这个 API 已被正式废弃,Chrome 90+、Firefox 84+ 中部分命令失效或行为错乱;更关键的是它和 JS 手动修改完全脱节:你调一次 document.execCommand('bold'),再按 Ctrl+Z,可能撤掉的是上一次用户打字,而不是刚加粗的动作。不同浏览器对“操作粒度”的判定也不一致,比如 Safari 可能合并连续输入,Edge 却拆成多步。
实际开发中应彻底弃用,改用状态快照 + 栈管理。
如何安全监听内容变更并存快照
仅靠 input 事件不够:中文输入法的 compositionstart/compositionend、粘贴(paste)、格式操作(如加粗)都不会触发 input;而 MutationObserver 又太敏感,DOM 细微变动就触发,容易存冗余快照。
- 监听组合事件:
input+mouseup(选区变化)+compositionend(中文输入完成) - 防抖保存:用
setTimeout延迟 200ms,避免连打时每键都存;只有editor.innerHTML确实变化才推入undoStack - 快照必须包含两部分:
innerHTML和当前光标位置(用getSelection().getRangeAt(0)序列化为路径 + 偏移)
撤销时还原光标位置比还原内容还难
直接赋值 editor.innerHTML = snapshot.html 后,光标会默认落到开头或末尾,用户编辑体验断裂。必须在存快照时同步记录选区,并在还原时重建:
- 存快照前调用
saveSelection(),把range.startContainer转为相对于 editor 的节点路径(如[0, 1, 2])和range.startOffset - 还原后用
restoreSelection(path, offset)递归查找节点并设置新 range - 注意:DOM 结构变化可能导致路径失效,需加容错(如 fallback 到文本匹配定位)
禁用原生撤销栈是必要前提
否则 JS 撤销和浏览器原生 Ctrl+Z 会互相干扰,典型现象是点一次 undo 按钮没反应,再按一次才生效,或者光标突然跳到奇怪位置。
必须在 keydown 中拦截:
editor.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && (e.key === 'z' || e.key === 'y')) {
e.preventDefault(); // 阻止原生行为
}
});
但注意别误伤其他快捷键(比如 Ctrl+Shift+Z 在某些系统是重做,要一并拦截)。
最易被忽略的一点:快照体积。存完整 innerHTML 字符串 + 选区路径,在长文档、高频编辑下内存增长极快。真实项目中必须限制 MAX_STEPS(建议 ≤ 50),且定期用 JSON.stringify() 对比前后快照,跳过无实质变化的保存。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











