IntersectionObserver 检测元素是否可见或元素间是否相交
过去,要检测一个元素是否可见或者两个元素是否相交并不容易,很多解决办法不可靠或性能很差。然而,随着互联网的发展,这种需求却与日俱增,比如,下面这些情况都需要用到相交检测:
- 图片懒加载——当图片滚动到可见时才进行加载
- 内容无限滚动——也就是用户滚动到接近内容底部时直接加载更多,而无需用户操作翻页,给用户一种网页可以无限滚动的错觉
- 检测广告的曝光情况——为了计算广告收益,需要知道广告元素的曝光情况
- 在用户看见某个区域时执行任务或播放动画
Element.getBoundingClientRect()
过去,相交检测通常要用到事件监听,并且需要频繁调用 Element.getBoundingClientRect() 方法以获取相关元素的边界信息。事件监听和调用 Element.getBoundingClientRect() 都是在主线程上运行,因此频繁触发、调用可能会造成性能问题。这种检测方法极其怪异且不优雅。
1 | window.addEventListener('scroll', checkScroll) |
假如有一个无限滚动的网页,开发者使用了一个第三方库来管理整个页面的广告,又用了另外一个库来实现消息盒子和点赞,并且页面有很多动画(译注:动画往往意味着较高的性能消耗)。两个库都有自己的相交检测程序,都运行在主线程里,而网站的开发者对这些库的内部实现知之甚少,所以并未意识到有什么问题。但当用户滚动页面时,这些相交检测程序就会在页面滚动回调函数里不停触发调用,造成性能问题,体验效果让人失望。
Intersection observer
Intersection Observer API 提供了一种异步检测目标元素与祖先元素或 viewport 相交情况变化的方法。它会注册一个回调函数,每当被监视的元素进入或者退出另外一个元素时 (或者 viewport ),或者两个元素的相交部分大小发生变化时,该回调方法会被触发执行。这样,我们网站的主线程不需要再为了监听元素相交而辛苦劳作,浏览器会自行优化元素相交管理。
Intersection Observer API 允许你配置一个回调函数和 options 对象,当以下情况发生时会调用回调函数:
- 每当目标 (target) 元素与设备视窗或者所指定的可滚动祖先元素发生交集的时候执行。如果元素不是可滚动元素的后代,则默认为设备视窗。设备视窗或者其他元素我们称它为根元素或根 (root)
- Observer 第一次监听目标元素的时候
1 | // new IntersectionObserver(callback, options) |
除了检测元素是否进入视口内,IntersectionObserver 还可以用来实现虚拟列表无限滚动。在下例中,当表格滑动到最底部的数据项时,需要请求下一页数据。
1 | const tableEl = document.querySelector('.ant-table-container') as HTMLElement; |
注意 Intersection Observer API 无法提供重叠的像素个数或者具体哪个像素重叠,他的更常见的使用方式是——当两个元素相交比例在 N% 左右时,触发回调,以执行某些逻辑。
options 配置
options 对象用来配置 callback 在什么场景下被调用。
- root: 指定根 (root) 元素,用于检查目标的可见性。必须是目标元素的父级元素。如果未指定或者为 null,则默认为浏览器视窗
- rootMargin: 根 (root) 元素的外边距。类似于 CSS 中的 margin 属性,比如 “10px 20px 30px 40px” (top、right、bottom、left)。如果有指定 root 参数,则 rootMargin 也可以使用百分比来取值。该属性值是用作 root 元素和 target 发生交集时候的计算交集的区域范围,使用该属性可以控制 root 元素每一边的收缩或者扩张。默认值为四个边距全是 0
- threshold: 目标 (target) 元素与根 (root) 元素之间的交叉度是交叉比 (intersection ratio)。这是目标 (target) 元素相对于根 (root) 的交集百分比的表示,它的取值在 0.0 和 1.0 之间。当 target 元素和 root 元素的交叉比达到 threshold 时,将执行对应的 callback。如果想检测当 target 元素在 root 元素中的可见性超过 50% 时,可以指定该属性值为 0.5。如果想要 target 元素在 root 元素的可见程度每多 25% 就执行一次 callback,那么可以指定一个数组 [0, 0.25, 0.5, 0.75, 1]。默认值是 0 (意味着只要有一个 target 像素出现在 root 元素中,回调函数将会被执行)。该值为 1.0 时,即当 target 完全出现在 root 元素中才会执行 callback
IntersectionObserverEntry
callback 接收 IntersectionObserverEntry 对象和观察者的列表:
1 | let callback =(entries, observer) => { |
- boundingClientRect: 返回包含目标元素的边界信息的 DOMRectReadOnly. 边界的计算方式与 Element.getBoundingClientRect() 相同
- intersectionRect: 返回一个 DOMRectReadOnly 用来描述根和目标元素的相交区域
- intersectionRatio: 返回 intersectionRect 与 boundingClientRect 的比例值
- isIntersecting: 返回一个布尔值,如果目标元素与交叉区域观察者对象 (intersection observer) 的根相交,则返回 true .如果返回 true, 则 IntersectionObserverEntry 描述了变换到交叉时的状态; 如果返回 false, 那么可以由此判断,变换是从交叉状态到非交叉状态
性能优势
IntersectionObserver(交叉观察器)和监听页面滑动事件结合 getBoundingClientRect 相比,具有以下性能优势:
减少回调次数:监听页面滑动事件结合 getBoundingClientRect 需要在每次滑动时计算目标元素的位置并触发回调,而 IntersectionObserver 可以在元素进入或离开视口时才触发回调,从而减少了不必要的计算和回调次数
延迟执行回调:当页面滑动速度较快时,监听页面滑动事件结合 getBoundingClientRect 容易触发频繁的回调,从而导致性能问题。而 IntersectionObserver 可以设置 threshold 属性来延迟回调的执行,从而减少了回调的频率
减少对页面性能的影响:监听页面滑动事件结合 getBoundingClientRect 需要在每次滑动时计算目标元素的位置,而这种计算会对页面的性能产生一定的影响。而 IntersectionObserver 可以在主线程之外执行,从而不会对页面的性能产生明显的影响
综上所述,IntersectionObserver 相比于监听页面滑动事件结合 getBoundingClientRect 具有更高的性能,特别是在需要监听多个元素时,使用 IntersectionObserver 可以大幅减少不必要的计算和回调次数,从而提高页面性能。
参考资料