MutationObserver、IntersectionObserver 和 ResizeObserver 分别解决什么问题?
简化版
MutationObserver 监听 DOM 结构或属性变化,IntersectionObserver 监听元素是否进入视口或容器,ResizeObserver 监听元素尺寸变化。它们都比轮询或频繁同步读取更适合性能敏感场景,但回调中仍要避免重计算和循环触发。
详细版
三者对比:
- MutationObserver:DOM 子节点、属性、文本变化。
- IntersectionObserver:曝光、懒加载、无限滚动、进入视口。
- ResizeObserver:容器尺寸变化、响应式组件、图表自适应。
示例:
const io = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) loadImage(entry.target)
})
})
io.observe(img)
面试回答要强调:Observer API 是浏览器提供的异步观察机制,能减少 scroll/resize 事件里手动计算带来的性能压力。
完整版教学
一、为什么需要 Observer API
传统做法里,很多需求靠轮询、scroll 事件、resize 事件或手动读取 DOM 完成。例如图片懒加载要不断判断元素是否进入视口,图表自适应要监听窗口变化,DOM 插件要检测节点插入。
window.addEventListener('scroll', () => {
const rect = img.getBoundingClientRect()
if (rect.top < window.innerHeight) loadImage(img)
})
这种方式容易频繁触发布局读取,scroll 高频时还会让主线程压力变大。Observer API 的价值是把观察逻辑交给浏览器调度,让回调在合适时机批量触发。
面试里要说出“异步、批量、由浏览器调度”,而不是只背 API 名字。
二、MutationObserver 观察 DOM 变化
MutationObserver 用来观察 DOM 节点的子节点、属性和文本变化。它替代了旧的 Mutation Events,适合富文本、插件容器、动态节点监控等场景。
const observer = new MutationObserver((records) => {
for (const record of records) {
console.log(record.type)
}
})
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
})
如果页面一次插入 100 个节点,Observer 回调通常会收到一批记录,而不是每个变化立刻同步触发一次业务逻辑。批处理能降低开销。
但不要滥用全页面 subtree: true。大型页面 DOM 频繁变化时,观察整个 body 的成本很高,应尽量缩小观察范围。
三、IntersectionObserver 观察可见性
IntersectionObserver 用来观察目标元素和视口或祖先容器的交叉情况。它非常适合图片懒加载、曝光埋点、无限滚动、动画触发。
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src
observer.unobserve(entry.target)
}
}
}, {
root: null,
threshold: 0.1
})
threshold: 0.1 表示交叉比例达到 10% 左右时触发。rootMargin 可以提前触发,例如图片距离视口还有 200px 时开始加载。
视口底部
+ rootMargin 200px
-> 提前加载图片
相比 scroll 里不断 getBoundingClientRect(),IntersectionObserver 更符合浏览器优化路径。
四、ResizeObserver 观察元素尺寸
ResizeObserver 监听元素尺寸变化,不是窗口尺寸变化。它适合组件级响应式,例如图表容器宽度变化、卡片布局变化、编辑器区域变化。
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const width = entry.contentRect.width
chart.resize(width)
}
})
ro.observe(chartContainer)
过去常用 window.resize,但窗口没变不代表元素没变。侧边栏展开、父容器布局改变、字体加载完成,都可能导致元素尺寸变化。
数字例子:一个 dashboard 有 12 个图表,侧栏从 240px 收起到 64px,窗口宽度没变,但图表容器都变宽了。ResizeObserver 能直接观察容器,而不是猜测所有布局来源。
五、三种 Observer 的选择对比
选择 API 时先问“我在观察什么变化”。DOM 结构变了用 MutationObserver,元素可见性变了用 IntersectionObserver,元素尺寸变了用 ResizeObserver。
| API | 观察对象 | 典型场景 |
|---|---|---|
| MutationObserver | DOM 结构、属性、文本 | 富文本、插件、动态节点 |
| IntersectionObserver | 元素与视口或容器交叉 | 懒加载、曝光、无限滚动 |
| ResizeObserver | 元素尺寸 | 图表自适应、组件布局 |
这三者都不是业务状态管理工具。如果你只是想知道 React/Vue 状态变了,不应该用 MutationObserver 反向观察 DOM,而应在框架状态层处理。
记忆钩子:Mutation 看“变没变”,Intersection 看“看没看见”,Resize 看“大小变没变”。
六、回调里要避免循环和重任务
Observer 回调虽然由浏览器调度,但回调里的代码仍运行在主线程。重计算、频繁 setState、同步布局读取都可能造成卡顿。
const ro = new ResizeObserver(() => {
// 危险:回调里改尺寸,可能继续触发 resize
box.style.width = box.offsetWidth + 1 + 'px'
})
ResizeObserver 特别容易出现“观察尺寸 -> 回调改尺寸 -> 再触发观察”的循环。解决方式是判断变化阈值、节流、使用 rAF 批量写入,或避免在回调中直接改变被观察元素尺寸。
Observer callback
-> 收集变化
-> requestAnimationFrame 批量更新
七、常见误区与追问
- 误区:Observer API 回调不占主线程。 回调仍执行在主线程,重逻辑照样会卡。
- 误区:IntersectionObserver 可以替代所有滚动逻辑。 它适合可见性判断,不适合需要逐像素滚动进度的复杂动画。
- 误区:ResizeObserver 监听的是窗口大小。 它监听元素尺寸,窗口 resize 只是尺寸变化来源之一。
- 追问:图片懒加载为什么适合 IntersectionObserver? 浏览器能批量计算交叉状态,避免 scroll 中频繁手动测量。
- 追问:MutationObserver 观察整个 body 有什么风险? 大页面变化频繁时记录量和回调成本很高。
- 追问:Observer 不用时要做什么? 调用
unobserve或disconnect,避免无意义观察和引用残留。
八、加强记忆
Observer API 按“三看”记:Mutation 看 DOM 变化,Intersection 看可见性,Resize 看尺寸。它们让浏览器帮你异步批量观察,但不会帮你消除回调成本。范围要小,回调要轻,不用要断开。