Web中原生内置的Observer方法们:MutationObserver、IntersectionObserver与ResizeObserver

827 阅读7分钟

在现代Web开发中,Observer(观察者)模式是实现事件驱动架构的一种重要方式。H5及相关的JavaScript API引入了多种Observer,使得开发者能够更加高效地监听DOM变化、Intersection状态等。下面,本文将介绍几种重要的Observer及其使用示例。

1. MutationObserver

MutationObserver 提供了一种监听DOM树变化的方法。无论是元素属性的更改、文本内容的变化还是子节点的添加和删除,都可以通过它来捕获。

示例代码:

// 选择需要观察的目标节点
var targetNode = document.getElementById('some-id');

// 配置观察选项:
var config = { attributes: true, childList: true, subtree: true };

// 当观察到变动时的回调函数
var callback = function(mutationsList, observer) {
    for(var mutation of mutationsList) {
        if (mutation.type == 'childList') {
            console.log('A child node has been added or removed.');
        } else if (mutation.type == 'attributes') {
            console.log('The ' + mutation.attributeName + ' attribute was modified.');
        }
    }
};

// 创建一个观察器实例并传入回调函数
var observer = new MutationObserver(callback);

// 开始观察目标节点
observer.observe(targetNode, config);

// 停止观察
// observer.disconnect();

相关属性

  • mutation.type:表示发生的变化类型。常见的类型包括 attributes(属性变更)、characterData(字符数据变更)、childList(子节点变更)。
  • mutation.target:返回被添加或移除的节点,或者属性被更改的节点。
  • mutation.addedNodes / mutation.removedNodes:分别返回被添加或移除的节点列表。
  • mutation.attributeName / mutation.oldValue:当 mutation.typeattributes 时,attributeName 表示哪个属性发生了变化,而 oldValue 则提供该属性变化前的值。

相关方法

  • observe(target, options):开始观察指定的目标节点。其中 options 参数是一个对象,可以包含如下选项:

    • childList: 布尔值,设置是否监视直接子节点的变动。
    • attributes: 布尔值,设置是否监视属性的变动。
    • characterData: 布尔值,设置是否监视数据(Text节点)的变动。
    • subtree: 布尔值,设置是否监视目标节点的所有后代节点的变动,默认为 false
    • attributeOldValue: 如果为 true,则在 mutation 对象中会包含 oldValue 属性来表示旧的属性值。
    • characterDataOldValue: 类似于 attributeOldValue,但是针对字符数据的旧值。
    • attributeFilter: 数组,限制仅监听特定属性的变化。
  • disconnect():停止观察所有目标节点。调用此方法后,将不会再有新的突变记录被传递给回调函数,直到再次调用 observe() 方法。

  • takeRecords():清空队列中的所有突变记录,并返回这些记录。这对于希望在断开连接之前获取所有未处理的突变信息非常有用。

在一些手写Promise的面试题里可能会用到这个MutationObserver来去模拟提供任务进入微队列的服务。

MutationObserver 不仅提供了监听DOM变化的强大功能,还利用了JavaScript运行时的特性——微任务队列(Microtask Queue),来确保所有DOM操作都已完成之后再触发观察者回调。这种机制是基于浏览器的事件循环模型,其中微任务队列优先于宏任务队列(如setTimeout、UI渲染等)执行。这意味着当DOM发生变化时,相应的MutationObserver回调不会立即执行,而是被安排在一个微任务中,在当前任务的所有同步代码执行完毕后,下一次事件循环开始前执行。这种方式保证了即使在连续的DOM变更情况下,MutationObserver也只会触发一次回调,极大地提升了性能和效率。

2. IntersectionObserver

IntersectionObserver 可以用来异步观察目标元素与祖先元素或顶级文档视口(viewport)交叉(即可见性)的情况。这对于实现懒加载、无限滚动等功能非常有用。

示例代码:

let observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if(entry.isIntersecting) {
      console.log('元素进入视口');
      // 加载图片或其他资源
    } else {
      console.log('元素离开视口');
    }
  });
});

let target = document.querySelector('#target-element');
observer.observe(target);

相关属性

IntersectionObserver 的回调函数中,IntersectionObserverEntry 对象提供了丰富的信息来描述目标元素的可见性变化:

  • isIntersecting:布尔值,表示目标是否当前与视口相交(即是否可见)。这是一个方便的快捷方式,避免了手动比较 intersectionRatio 是否大于0。
  • intersectionRatio:一个介于0到1之间的数值,表示目标元素与视口重叠的比例。当这个值为0时,意味着元素完全不在视口中;当值为1时,表示元素完全位于视口内。
  • boundingClientRect:提供目标元素的大小及其相对于视口的位置。
  • rootBounds:如果根元素是视口,则返回视口的大小;如果指定了自定义根元素,则返回该元素的大小。
  • intersectionRect:表示两个矩形(目标元素和根元素)的交集部分的大小和位置。
  • target:被观察的目标元素。

相关方法

  • observe(target):开始观察指定的目标元素。你可以对多个元素调用此方法以同时观察它们。

  • unobserve(target):停止观察特定的目标元素。当你不再需要监听某个元素的变化时,可以使用此方法取消观察。

  • disconnect():停止观察所有目标元素。一旦调用了此方法,就不会再有新的交叉状态变更触发回调,直到再次调用 observe() 方法。

  • 观察选项(options)

    • root: 指定作为参照容器的元素,默认是浏览器视窗。如果要监控元素相对于另一个滚动容器的可见性,可以在这里指定那个容器。
    • rootMargin: 类似于CSS中的margin,用来扩展或缩小根(root)元素的边界。默认值为 "0px 0px 0px 0px"。
    • threshold: 可以是一个单一的数字或者一个数组,用来指定触发回调的交叉比例。例如,值为0.5表示当至少一半的目标元素出现在视口中时触发回调。默认值为0,意味着只要有任何部分进入视口就会触发回调。

IntersectionObserver 提供了一种高效且灵活的方式来处理元素的可见性变化,使得我们可以轻松实现诸如图片懒加载、广告曝光统计等复杂功能。此外,由于它是基于异步通知机制设计的,因此相比传统的事件监听方式更加节省资源。

3. ResizeObserver

ResizeObserver 用于监听元素尺寸变化。这在响应式设计中特别有用,当用户调整浏览器窗口大小或者动态改变某个元素的大小时,可以触发相应的回调处理逻辑。

示例代码:

const resizeObserver = new ResizeObserver(entries => {
  for (let entry of entries) {
    console.log('Element:', entry.target);
    console.log('New size:', entry.contentRect.width, 'x', entry.contentRect.height);
  }
});

// 观察一个指定的元素
resizeObserver.observe(document.querySelector('aside'));

相关属性

ResizeObserver 的回调函数中,ResizeObserverEntry 对象提供了有关被观察元素尺寸变化的信息:

  • contentRect:提供了一个 DOMRectReadOnly 对象,包含了目标元素的内容框(不包括边框、内边距或外边距)的相关信息,如宽度(width)、高度(height)、顶部(top)、左侧(left)等。这是最常用的部分,用来获取元素的实际内容区域尺寸。
  • target:表示被观察的目标元素本身。
  • 还有部分不太常用的属性,可以直接打印对象查看。

相关方法

  • observe(target):开始观察指定的目标元素。可以对多个元素调用此方法以同时观察它们。例如:

    resizeObserver.observe(document.querySelector('aside'));
    
  • unobserve(target):停止观察特定的目标元素。当你不再需要监听某个元素的变化时,可以使用此方法取消观察。这对于性能优化非常有用,因为它允许你根据应用的状态动态管理观察者。

  • disconnect():停止观察所有目标元素。一旦调用了此方法,就不会再有新的尺寸变更触发回调,直到再次调用 observe() 方法。这在你确定不需要继续监听任何元素的尺寸变化时很有用,可以帮助释放资源。

此外,值得注意的是,ResizeObserver 不仅可以监听直接的尺寸变化,还可以通过监听嵌套的子元素来间接感知布局变化的影响。这意味着即使父容器的尺寸没有直接改变,如果它的子元素导致了重新布局,ResizeObserver 也能检测到这种变化。