C07 观察者与 Web API

2026-10-31 17:030 阅读约 16 分钟JavaScript

C07 观察者与 Web API

摘要:三类观察者替代了轮询与全局监听:IntersectionObserver 在元素进入视口时回调,用于懒加载与曝光统计;ResizeObserver 监听元素尺寸变化,用于图表与自适应组件;MutationObserver 监听 DOM 结构或属性变化,用于第三方脚本适配。三者都在浏览器渲染前批量回调,不占用每次滚动的计算。本文给出三类观察者的完整写法、回调时机与两个补充 API(requestIdleCallback、matchMedia)。
关键词:IntersectionObserver,ResizeObserver,MutationObserver,懒加载,空闲回调

所属:frontend/js / C-高级 | 上一篇:C06-深拷贝与结构化克隆 | 下一篇:C08-History与前端路由 | 返回:C00-总结

1 引言

滚动事件里读取元素位置、窗口 resize 里重算布局、setInterval 检查 DOM 变化,三者的共同问题是:在主线程上反复读取布局,每次都可能触发同步布局计算。

本文给出三类观察者 API 的替代写法。范围见表 1-1。

项 本文覆盖 本文不覆盖
可见性 IntersectionObserver 阈值与根元素 虚拟列表实现
尺寸 ResizeObserver 与循环触发的规避 容器查询(CSS C01)
结构 MutationObserver 配置与批量处理 框架变更检测机制
补充 requestIdleCallback、matchMedia 性能剖析(C03)

表 1-1 本文覆盖范围

2 预备知识

项 要求 核对方式
环境 Chrome 130+,本地服务器 页面经 http 打开
前置 A08、C03 DOM 与长任务已掌握
文件 observe/index.html、observe/app.js 编码 UTF-8

表 2-1 环境与前置要求

3 原理与实现

3.1 观察者由浏览器在渲染前批量回调

注册后由浏览器在合适时机统一回调,不在每次滚动或每次 DOM 变更时同步执行,如图 3-1 所示。

flowchart TD
    A["注册观察者"] --> B["浏览器在后台跟踪目标"]
    B --> C{"达到阈值或发生变化"}
    C -->|否| B
    C -->|是| D["把变更放入队列"]
    D --> E["渲染前的回调阶段批量执行"]
    E --> F["回调内收到 entries 数组"]
    F --> G["回调中避免同步读布局,防止循环触发"]

图 3-1 观察者的回调时机位于渲染之前

图上可见:回调收到的是批量条目而非单个事件,回调内修改被观察的属性会再次排队,形成循环。

3.2 完整实现

<!-- observe/index.html;运行环境:Chrome 130+ -->
<div class="spacer">向下滚动</div>
<img class="lazy" data-src="/img/a.jpg" alt="A" width="800" height="450" />
<img class="lazy" data-src="/img/b.jpg" alt="B" width="800" height="450" />
<div id="chart" class="chart"></div>
<div id="third-party"></div>
// observe/app.js;运行环境:Chrome 130+
// 1 懒加载:元素进入视口前不发起请求
const lazyObserver = new IntersectionObserver(
  (entries, observer) => {
    for (const entry of entries) {
      if (!entry.isIntersecting) continue;
      const img = entry.target;
      img.src = img.dataset.src;
      img.removeAttribute('data-src');
      observer.unobserve(img);            // 加载后停止观察
    }
  },
  { root: null, rootMargin: '200px 0px', threshold: 0.01 },
);

for (const img of document.querySelectorAll('.lazy')) lazyObserver.observe(img);

// 2 曝光统计:达到可见比例才算一次曝光
const exposure = new IntersectionObserver(
  (entries) => {
    for (const entry of entries) {
      if (entry.intersectionRatio >= 0.5) {
        console.log('exposed', entry.target.id);
        exposure.unobserve(entry.target);
      }
    }
  },
  { threshold: [0.5] },
);

// 3 尺寸变化:回调内只改不读,避免循环触发
const chart = document.querySelector('#chart');
const resizeObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { inlineSize, blockSize } = entry.contentBoxSize[0];
    // 写入尺寸,不在回调里读 offsetWidth
    chart.dataset.w = String(Math.round(inlineSize));
    chart.dataset.h = String(Math.round(blockSize));
  }
});
resizeObserver.observe(chart);

// 4 DOM 变更:第三方脚本插入内容后做适配
const mutationObserver = new MutationObserver((records) => {
  for (const record of records) {
    if (record.type === 'childList' && record.addedNodes.length > 0) {
      const added = [...record.addedNodes].filter((n) => n.nodeType === 1);
      for (const node of added) node.classList.add('normalized');
    }
  }
});

mutationObserver.observe(document.querySelector('#third-party'), {
  childList: true,
  subtree: true,
  attributes: true,
  attributeFilter: ['class'],
});

// 5 空闲时间执行低优先级任务
const pending = [() => console.log('低优先级任务')];

requestIdleCallback(
  (deadline) => {
    while (deadline.timeRemaining() > 1 && pending.length > 0) {
      pending.shift()();
    }
  },
  { timeout: 2000 },
);

// 6 媒体查询变化:跟随系统偏好
const dark = matchMedia('(prefers-color-scheme: dark)');
dark.addEventListener('change', (event) => {
  document.documentElement.dataset.theme = event.matches ? 'dark' : 'light';
});

// 7 卸载时统一断开
const controller = new AbortController();
window.addEventListener('pagehide', () => {
  lazyObserver.disconnect();
  resizeObserver.disconnect();
  mutationObserver.disconnect();
  controller.abort();
}, { signal: controller.signal });

三类观察者的配置与用途见表 3-1。

观察者 触发条件 关键配置 典型用途
IntersectionObserver 目标与根的相交比例变化 root、rootMargin、threshold 懒加载、曝光、吸顶
ResizeObserver 目标内容盒尺寸变化 box: 'content-box' 图表、自适应组件
MutationObserver 子节点或属性变化 childList、attributes、subtree 第三方脚本适配

表 3-1 三类观察者的触发条件与配置

与旧做法的对比见表 3-2。

需求 旧做法 观察者方案 差异
判断元素是否可见 滚动事件 + getBoundingClientRect IntersectionObserver 不再每次滚动同步读布局
跟踪元素尺寸 window.resize + 读 offsetWidth ResizeObserver 能感知元素自身而非视口
感知 DOM 插入 setInterval 轮询 MutationObserver 变更即回调,无空转
低优先级任务 setTimeout(0) requestIdleCallback 让出主线程给交互

表 3-2 观察者与传统写法的差异

4 验证与讨论

  1. 打开页面并向下滚动,确认图片在进入视口前 200 像素时开始加载。
  2. 在开发者工具中改变 #chart 的宽度,确认 data-w 随之更新且控制台无循环警告。
  3. 在控制台执行 document.querySelector('#third-party').append(document.createElement('span')),确认新节点被加上 normalized 类。
  4. 切换系统的深色模式,确认 data-theme 自动变化。
检查项 预期结果 说明
懒加载 未进入视口时不发起请求 3.2 节
阈值生效 可见比例达标才计曝光 表 3-1
尺寸跟踪 元素宽度变化即回调 表 3-2
DOM 适配 动态插入节点被处理 3.2 节
清理 disconnect 后不再回调 3.2 节第 7 段

表 4-1 五项验收

讨论与局限。 ResizeObserver 回调内若同步读取布局属性会触发 ResizeObserver loop completed with undelivered notifications 警告,做法是把读写分离到下一帧。另,IntersectionObserver 的首帧回调在观察开始后异步到达,首屏元素需要单独处理初始状态。

5 常见问题

现象 原因 处理
懒加载图片不显示 data-src 未替换或路径错误 检查 Network 面板与属性值
尺寸回调报循环警告 回调内同步读布局 只写不读,或放到 requestAnimationFrame
首屏元素未触发可见回调 初始回调异步到达 初始化时先按可见处理一次
MutationObserver 漏掉变化 未开 subtree 或过滤了属性 补齐配置与 attributeFilter
组件销毁后仍回调 未 disconnect 在卸载钩子中统一断开
空闲回调从不执行 页面持续繁忙 加 timeout 保证兜底执行

表 5-1 观察者的六类问题

6 结论

序号 结论 依据
1 可见性与尺寸判断一律用观察者,不用滚动事件 表 3-2
2 回调内只写不读,避免循环触发 图 3-1
3 观察完成后 unobserve 或 disconnect 3.2 节
4 低优先级任务放 requestIdleCallback 表 3-2

表 6-1 结论汇总

参考文献

[1] MDN Web Docs · Intersection Observer API,https://developer.mozilla.org/zh-CN/docs/Web/API/Intersection_Observer_API(访问于 2026-10-31)
[2] MDN Web Docs · MutationObserver,https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver(访问于 2026-10-31)

返回文章列表

评论 0

0/2000

还没有评论,快来抢沙发吧~