JavaScript 中的 Observer API
Notes about Observer APIs in JavaScript
最近,我要在网页中需要实现一个功能。它的背景是这样,我们在页面顶部有一个左右滑动的导航条 nav,当用户在往下滚动页面时,我们会给导航条中对应的项目增加一个名为 active 的 class,当这个 active item 没有完全在页面上展示时,我们需要将那个 item 水平滑动到屏幕中间。用户也可以手动点击导航条中的项目,此时我们需要将页面上下滚动到对应的位置。
这里就涉及到两个问题:
我们需要监控每个 nav item,知道 active 这个 class 被添加上去了;
对于 nav item,我们需要判断它是否在浏览器可见区域内完全可见。
在开发这个功能的过程中,我了解到了 Javascript 里有 Observer APIs 这个概念。与传统的在主进程中监听事件的方法相比,Observer APIs,顾名思义,利用了观察者模式,当一个对象的状态发生改变时,其所有依赖者都会收到通知并自动更新。并且,Javascript 中的 Observer APIs 并非在主线程中执行,因此可以提升性能。
常见 Observer APIs
我们常用的 Observer API 有这几个:
- MutationObserver
- IntersectionObserver
- ResizeObserver
如果我们去搜索的话,还可以看到 PerformanceObserver
和 ReportingObserver
这两个观察器在一般的功能实现中并不太常用。
在文章开头提到的那个功能中,我就用到了 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 的主要功能是当一个元素的可见位置发生变化时,比如滑入或者离开可见区域,我们可以进行监听并执行一些方法。因此,它可以完成这些功能:
无限滚动:当页面滚动到底部时,我们通过 ajax 去请求下一页的元素,避免了翻页,实现无限加载
图片懒加载:当页面滚到的图片到可见区域时,我们才去加载图片
更具体的功能和使用,可以参考其文档。
小结
以上就是我对Observer APIs的探索过程,它们可用于实现涉及监控和响应 DOM 变化的复杂功能。MutationObserver 在检测类的变化方面发挥了作用,当导航栏中的某个项目获得 "active" 类时,它可以做出动态响应。使用 IntersectionObserver 进一步扩展了这一功能,以保证 active 导航元素是否在浏览器中完全可见。
学习Observer APIs可以丰富我们的工具箱,用于创建高效的 web app。