mutationrecord 是回溯唯一可靠的变更来源,因其能真实反映dom树的原子级改动,包括新增节点、删节点、改文本、换属性,粒度细至单个text节点内容变化。

为什么 MutationRecord 是回溯唯一可靠的变更来源
HTML 编辑器的“撤销/重做”不能依赖 input 或 keydown,因为它们捕获不到粘贴、拖拽、格式化、光标跳转后插入等行为;window.history 只管 URL,完全不记录 DOM 变更。只有 MutationRecord 能真实反映每次 DOM 树的原子级改动——新增节点、删节点、改文本、换属性,粒度细到单个 Text 节点内容变化。
怎么从 MutationRecord 里提取可回溯的变更快照
MutationRecord 不是直接存“差量”,它只告诉你发生了什么类型的操作和影响了哪个节点。要构建可还原的状态,必须在回调中主动提取并结构化信息:
- 对
type === 'childList':遍历mutation.addedNodes和mutation.removedNodes,用node.cloneNode(true)深拷贝(注意过滤DocumentFragment和注释节点) - 对
type === 'characterData':读mutation.target.textContent(当前值)和mutation.oldValue(需配置characterDataOldValue: true),二者差值即本次文本变更 - 对
type === 'attributes':用mutation.attributeName定位字段,mutation.oldValue/target.getAttribute(mutation.attributeName)分别取旧新值 - 所有快照必须带时间戳和唯一
id,避免多步操作合并时丢失顺序
容易踩坑的三个关键细节
很多回溯实现中途崩溃或状态错乱,问题几乎都出在这三点:
-
subtree: true必须开——编辑器里<strong>hello</strong>的文本实际在<strong></strong>的子Text节点里,不开subtree就收不到 -
takeRecords()要在每次回溯前调用——否则积压的未处理 mutation 会在下一次 observe 时批量触发,导致“撤销一步却还原三步” - 别在回调里直接改
mutation.target——比如给刚 added 的节点加 class,会触发新一轮 mutation,极易无限循环;应先缓存变更,统一在节流后批量应用
回溯还原时如何精准定位并复原
还原不是简单地 innerHTML = snapshot,那样会丢光标、选区、事件绑定。真正可用的方式是基于 MutationRecord 的 target 和 previousSibling/nextSibling 做局部 DOM 替换:
- 还原
childList类型:用target.insertBefore(newNode, nextSibling)或target.removeChild(removedNode),保持原有父节点和兄弟关系 - 还原
characterData类型:直接赋值mutation.target.textContent = oldValue,比操作父元素安全得多 - 还原
attributes类型:用target.setAttribute(attr, oldValue),而不是target.className = ...(后者可能触发额外 classList 变更) - 还原前务必检查
mutation.target.isConnected——节点可能已被其他操作移除,强行操作会报错
最常被忽略的是:MutationRecord 本身不包含光标位置信息,回溯后必须手动用 getSelection() + Range 恢复焦点,否则用户会卡在错误位置。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











