Hegwin.Me

朱雀桥边野草花,乌衣巷口夕阳斜。

JavaScript 中的 Observer API

Notes about Observer APIs in JavaScript

最近,我要在网页中需要实现一个功能。它的背景是这样,我们在页面顶部有一个左右滑动的导航条 nav,当用户在往下滚动页面时,我们会给导航条中对应的项目增加一个名为 active 的 class,当这个 active item 没有完全在页面上展示时,我们需要将那个 item 水平滑动到屏幕中间。用户也可以手动点击导航条中的项目,此时我们需要将页面上下滚动到对应的位置。

这里就涉及到两个问题:

  1. 我们需要监控每个 nav item,知道 active 这个 class 被添加上去了;

  2. 对于 nav item,我们需要判断它是否在浏览器可见区域内完全可见。

在开发这个功能的过程中,我了解到了 Javascript 里有 Observer APIs 这个概念。与传统的在主进程中监听事件的方法相比,Observer APIs,顾名思义,利用了观察者模式,当一个对象的状态发生改变时,其所有依赖者都会收到通知并自动更新。并且,Javascript 中的 Observer APIs 并非在主线程中执行,因此可以提升性能。

常见 Observer APIs

我们常用的 Observer API 有这几个:

  • MutationObserver
  • IntersectionObserver
  • ResizeObserver

如果我们去搜索的话,还可以看到 PerformanceObserverReportingObserver 这两个观察器在一般的功能实现中并不太常用。

在文章开头提到的那个功能中,我就用到了 MutationObserver 和 IntersectionObserver,接下来具体讲讲我是怎么使用的。

MutationObserver

我需要知道 nav 下面的 item 是否获得了 active class,我就可以这样来判断:

// Create a Mutation Observer instance
const observer = new MutationObserver((mutationsList, observer) => {
  for (const mutation of mutationsList) {
    // Check if the class attribute has changed on the observed element
    if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
      const hasActiveClass = targetElement.classList.contains('active');
      // logic for handling item with the 'active' class
    }
  }
});

MutationObserver 的 constructor 接受一个 callback function 作为参数,我们可以把这个 function 拿出来,单独定义一下。

//  callback function
function handleActiveClassChange(mutationsList) {
  mutationsList.forEach((mutation) => {
    if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
      const targetElement = mutation.target;
      if (targetElement.classList.contains('active')) {
        // logic for handling item with the 'active' class
      }
    }
  });
}

// Create a MutationObserver
const observer = new MutationObserver(handleActiveClassChange);

此时,我们生成了一个 MutationObserver,但是我们还没有用它来“观察”任何元素,我们需要调用它的实例方法 observe() 来监控 nav 下面的元素。

const config = {
  attributes: true,
  attributeFilter: ['class'],
};

// Add your links to the observer
const links = document.querySelectorAll('nav .item');
links.forEach((link) => {
  observer.observe(link, config);
});

利用这个 MutationObserver 我们就可以知道导航栏中哪个 item 发生了class的变化,并做出后续的功能了;我们可以查看其文档获取更多信息。

IntersectionObserver

在知道了我们需要对那个 nav item 进行处理之后,下一个功能点就是判断 active nav item 是否在浏览器可见区域内完全展示。在过去的做法里,我们可以利用 getBoundingClientRect() 的盒子模型进行判断,如下:

const isElementFullyVisible = (element) => {
  const rect = element.getBoundingClientRect();
  return (
    rect.left >= 0 &&
    rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  );
}

这个方法依然是可以工作的,但是它运行在主进程里,当用户在滚动页面时,频繁触发、调用可能会造成性能问题。

而当我们利用 Intersection Observer时,我们可以这样去给每个元素注册观察者:

// Target elements we want to observe
const navItems = document.querySelectorAll('nav .item');

navItems.forEach((navItem) => {
  const observer = new IntersectionObserver(callback, options);
  observer.observe(navItem);
});

Intersection Observer 的 constructor 同样是接受 callback 和可选的options作为参数:

// Create an Intersection Observer
const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.intersectionRatio < 1) {
      // Element is not fully visible
      // Perform scrollToCenter here
      console.log('Element is not fully visible');
    }
  });
}, {
  // Set the threshold to detect when the element is not fully visible
  threshold: 1,
});

用来检测一个元素是否在完全可见并不是 Intersection Observer 主要设计意图——我的功能并非一个展示它所有能力的好例子——这是它可以完成的功能,但并非全部。Intersection Observer 的主要功能是当一个元素的可见位置发生变化时,比如滑入或者离开可见区域,我们可以进行监听并执行一些方法。因此,它可以完成这些功能:

  1. 无限滚动:当页面滚动到底部时,我们通过 ajax 去请求下一页的元素,避免了翻页,实现无限加载

  2. 图片懒加载:当页面滚到的图片到可见区域时,我们才去加载图片

更具体的功能和使用,可以参考其文档

小结

以上就是我对Observer APIs的探索过程,它们可用于实现涉及监控和响应 DOM 变化的复杂功能。MutationObserver 在检测类的变化方面发挥了作用,当导航栏中的某个项目获得 "active" 类时,它可以做出动态响应。使用 IntersectionObserver 进一步扩展了这一功能,以保证 active 导航元素是否在浏览器中完全可见。

学习Observer APIs可以丰富我们的工具箱,用于创建高效的 web app。

< Back