✅ 推荐方案:使用 MutationObserver 精准监听目标元素;相比 setTimeout(不可靠、易受网络/设备性能影响)或轮询 setInterval(低效且难终止),MutationObserver 是现代前端等待动态节点的标准解法,它能高效响应 DOM 变化,并在目标元素首次出现时立即触发回调。

直接监听 document.body 或整个页面做“导航守卫”不可行——它会把广告注入、客服浮窗、A/B测试脚本全当成“跳转”,误报率高且主线程卡死。真正能用的方案,是锁定关键区域、精配选项、回调里做轻量判定。
监听目标必须是具体容器,不是 document.body
用户点击侧边栏菜单、切换 Tab、加载新模块时,DOM 变动往往集中在某个容器内(比如 #main-content、.router-view、[data-router-outlet])。监听 document.body 会捕获所有无关变更:广告 SDK 插入 div#ad-banner、埋点脚本加 script[data-track]、甚至浏览器自动插入的 style[amp-bind]。这些都不是导航行为,但会触发你写的所有逻辑。
- 正确做法:用
document.querySelector('#main-content')获取真实挂载的容器,并校验target?.nodeType === 1 && target.isConnected - 若用框架(Vue/React),必须在
mounted或useEffect中获取 DOM 节点后再调用observe(),否则target是 null 或未挂载 - 不要监听
document:旧版 Safari 对它不触发childList,且会捕获head里的动态 script 注入,干扰更大
配置只开 childList + subtree,关掉 attributes 和 characterData
导航守卫关心的是“结构是否替换”,不是“按钮加了 class”或“段落改了颜色”。开 attributes: true 会让每次 el.classList.toggle('active') 都进回调;开 characterData: true 则连用户输入一个字都算“变动”,完全偏离目标。
- 必开:
childList: true(捕获新增/移除节点)、subtree: true(防止路由组件深层嵌套导致漏判,比如#main > .page > .content > article) - 禁用:
attributes: false、characterData: false——除非你要监控特定 data 属性(如data-route-id),那才单独加attributeFilter: ['data-route-id'] - 别信“全开更保险”:CPU 占用会明显上升,长页面中频繁触发回调直接卡顿
回调里只筛 addedNodes,用 nodeType 和 matches 做两级过滤
收到的 mutations 是批量记录,混着框架 diff、用户操作、甚至你自己刚插进去的节点。不筛就处理,极易误删、死循环或重复响应。
立即学习“前端免费学习笔记(深入)”;
- 第一层筛类型:
if (mutation.type !== 'childList') continue,跳过属性/文本变更 - 第二层筛节点:
Array.from(mutation.addedNodes).filter(node => node.nodeType === 1 && node.matches('[data-route]')),只认带路由语义的元素 - 避免空指针:
node.parentElement?.matches('.router-view')比直接node.parentElement.classList.contains(...)安全 - 修复前先
observer.disconnect(),操作完再observer.observe(),防自身改动触发新回调
组件卸载时必须 disconnect,且不能依赖 GC 自动回收
MutationObserver 实例持有对 DOM 节点的强引用,不手动断开,节点无法被垃圾回收。在单页应用中,频繁切换路由却没 disconnect(),内存缓慢上涨,几小时后页面就卡顿。
Vue 中写在 onBeforeUnmount,React 中写在 useEffect 返回函数里,Svelte 中用 onDestroy。别指望“页面刷新就清了”——SPA 里没有整页刷新,只有局部重载。



















