必须用 mutationobserver 监听预览容器的 dom 变更,启用 childlist: true 和 subtree: true;监听目标须为 #preview 元素,赋值 innerhtml 后再 observe,并在卸载时调用 disconnect()。

实时预览器中 DOM 结构变更必须用 MutationObserver 监听,且必须启用 childList: true 和 subtree: true,否则深层插入的节点(比如用户输入 HTML 后生成的嵌套结构)根本捕获不到。
监听目标必须是预览容器本身,不能是 document 或 body
预览器通常把用户输入渲染到一个隔离的 <div id="preview"> 里。如果监听 <code>document.body,会混入编辑器 UI、工具栏等无关节点,回调逻辑被干扰;监听 document 还可能因跨 iframe 权限报错。
- 正确做法:用
document.getElementById('preview')获取容器,确保它已挂载(可加if (!targetNode?.nodeType === 1) return防空指针) - 不要在 HTML 字符串解析前就调用
observe(),否则初始渲染的节点变动会被漏掉——应在innerHTML赋值完成后再启动监听 - 若预览区支持 iframe 模式,需在 iframe 加载完成后,取其
contentDocument.body作为 target,且注意同源限制
只处理结构变化,避免误响应 class/style 修改
用户改 class="btn" 或 style="color:red" 属于属性变更,不是结构变更。预览器关注的是“元素是否新增/移除/重排”,这类样式类改动不应触发重新高亮、语法校验或尺寸重算。
- 配置项只需
{ childList: true, subtree: true },**不要加attributes: true或characterData: true** - 回调中只检查
mutation.type === 'childList',忽略其他类型 - 遍历
mutation.addedNodes时,用Array.from(mutation.addedNodes).filter(node => node.nodeType === 1)确保只处理元素节点,跳过 Text、Comment 等
防止回调中操作 DOM 引发死循环
预览器常在监听到新节点后自动绑定事件、添加高亮 class 或插入调试标记。但这些操作本身又会触发新的 childList 变更,导致无限回调。
- 在回调开头加开关变量(如
if (isProcessing) return; isProcessing = true),处理完再置false - 更稳妥的做法:用
observer.takeRecords()清空前序未处理记录,再执行 DOM 操作,最后手动调用observer.observe()恢复监听 - 避免在回调里直接修改被监听的容器内容(如往
#preview里 append),应改用临时文档片段(document.createDocumentFragment())组装后一次性替换
组件卸载或切换预览模式时必须断开监听
用户关闭预览面板、切换到 Markdown 模式、或刷新编辑器状态时,若不主动断开,MutationObserver 实例仍持有对旧 DOM 节点的引用,造成内存泄漏,且后续回调可能报 Cannot read property 'nodeType' of null。
- 在销毁逻辑中明确调用
observer.disconnect(),不要依赖 GC - 如果预览器支持热重载(如保存即刷新预览),断开后需重建实例,不能复用旧 observer
- 可在控制台临时加
console.log('observer active:', observer.takeRecords().length > 0)辅助判断是否残留未处理变更
最易被忽略的是:预览容器首次渲染时,innerHTML 赋值和 observe() 的时序。DOM 写入是同步的,但浏览器批量更新是异步的,所以哪怕你紧挨着写 el.innerHTML = html; observer.observe(el, config),首次插入的节点也可能不会触发回调——得用 requestAnimationFrame 或 setTimeout(..., 0) 把 observe() 推到微任务之后。这点在 SSR 或服务端渲染回填场景下尤其关键。











