Intersection Observer API
Intersection Observer API
随着 Web 应用的丰富和成熟,检测元素是否可见的需求增多。之前一般是通过三方库来实现,各自有各自的实现方式,性能也有差异。Intersection Observer API 便是这种功能的一个原生支持。
适用场景
- 页面滚动过程中的懒加载。
- 长页面无限内容加载。
- 页面中广告查看量的计算。
- 以及其他需要用户可见才进行的操作,动画等。
用法及参数
初始化监视器
let options = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 1.0
}
let observer = new IntersectionObserver(callback, options);
使用该 API,有两个角色会参与进来,
- 容器(root or root element):缺省或传递
null
时为文档视窗 viewport,否则是指定的元素。 - 目标元素(target):需要监视(observe)的目标元素。
API 会在目标元素可见,即出现在容器中时执行指定的回调。
但回调也不是都执行,它受参数 threshold
的控制。
所以还需要理解一个概念,Intersection Ratio,可理解成目标元素的身体出现在容器中多少时,才触发回调,进一步理解请看下图:
Intersection Ratio 示例 -- 图片来自 Arnelle Balane 的文章 The Intersection Observer API
上面 threshold
就是指定 Intersection Ratio 的。它是一个介于 0~1 的小数,默认为 0 ,即一旦可见就执行,1 则表示元素全部可见才执行。
除了将 threshold
指定为数字,还可指定为一个数字的数组,比如 [0, 0.25, 0.5, 0.75, 1]
,这样在元素出现过程中,其可见范围分别为 0, 25%,50%,75%, 100% 时都会触发一次回调。
而 rootMargin
则用于指定容器的边距,接收的值和 CSS margin 一样。这个边距会在计算目标可见范围时被考虑进去,即在容器原有的内容范围内,减去 margin 的这部分。
执行监视
利用构造器 IntersectionObserver
创建好一个监视器后,便可以对容器内的元素进行监视了。
let target = document.querySelector('#listItem');
observer.observe(target);
回调
回调的结构如下:
let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
回调的入参为一个 IntersectionObserverEntry
数组,同时还有监视器本身 observer
。
注意:虽然监测器的监听是异步的,但回调的执行是在主线程,所以回调中不宜进行耗时任务,如果确实需要应该使用 Window.requestIdleCallback()
来执行。
一个完整的示例
实现一个页面无限滚动及图片懒加载。完整代码可在 wayou/intersection-observer-api 查看。
App.tsx
import React, { useState, useEffect, useCallback } from "react";
import "./App.css";
const IMAGE_API = "https://picsum.photos/200";
const PAGE_SIZE = 10;
const IMAGES_PAGED = new Array(PAGE_SIZE).fill(IMAGE_API);
let loaderObserver: IntersectionObserver;
let lazyLoadObserver: IntersectionObserver;
let options = {
rootMargin: "0px",
threshold: 0
};
let page = 0;
const App = () => {
const [images, setImages] = useState<string[]>(IMAGES_PAGED);
const infiniteCallback = useCallback<IntersectionObserverCallback>(
(_entries, _observer) => {
console.info(`loading page ${++page}`);
setImages(prev => [...prev, ...IMAGES_PAGED]);
},
[]
);
const lazyLoadCallback = useCallback<IntersectionObserverCallback>(
(entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// stop observer and load the image
const lazyImage = entry.target as HTMLImageElement;
console.log("lazy loading ", lazyImage);
lazyImage.classList.remove("empty");
lazyImage.src = lazyImage.dataset.src!;
observer.unobserve(entry.target);
}
});
},
[]
);
//setup infinite loading
useEffect(() => {
if (!loaderObserver) {
loaderObserver = new IntersectionObserver(infiniteCallback, options);
}
const target = document.querySelector("footer");
if (target) {
loaderObserver.observe(target);
}
return () => {
if (target) {
loaderObserver.unobserve(target);
}
};
}, [infiniteCallback]);
// setup lazy loading
useEffect(() => {
if (!lazyLoadObserver) {
lazyLoadObserver = new IntersectionObserver(lazyLoadCallback, options);
}
const imgs = document.querySelectorAll("img.empty");
if (imgs) {
imgs.forEach(img => {
console.count("setup img observer");
lazyLoadObserver.observe(img);
});
}
return () => {
if (imgs) {
imgs.forEach(img => {
lazyLoadObserver.unobserve(img);
});
}
};
}, [lazyLoadCallback, images]);
return (
<div className="App">
<div className="content-wrap">
{images.map((image, index) => (
<img className="empty" key={index} data-src={image + `?f=${index}`} />
))}
</div>
<footer>loading more ....</footer>
</div>
);
};
export default App;
运行效果:
利用 Intersection Observer API 实现的无限图片懒加载运行效果
兼容性
虽然是实验性 API,但支持情况还可以。根据 Can I Use #intersectionobserver 的数据,PC 端除 IE 外主流浏览器均已支持,移动端 UC 部分支持,其余浏览器支持良好。