我们经常遇到这样的需求——检测一个元素是否可见或者两个元素是否相交,如
● 图片懒加载——当图片滚动到可见时才进行加载
● 内容无限滚动——也就是用户滚动到接近内容底部时直接加载更多,而无需用户操作翻页,给用户一种网页可以无限滚动的错觉
● 检测广告的曝光情况——为了计算广告收益,需要知道广告元素的曝光情况
● 在用户看见某个区域时执行任务或播放动画
Element.getBoundingClientRect()
返回一个 DOMRect 对象,其提供了元素的大小及其相对于视口的位置。
缺点:事件监听和调用都是在主线程上运行,因此频繁触发、调用,造成浏览器频繁的重绘和回流,给网站带来相当大的卡顿
Intersection Observer API
window对象上有一个Intersection Observer API,它会注册一个回调函数,每当被监视的元素进入或者退出另外一个元素时 (或者 viewport ),或者两个元素的相交部分大小发生变化时,该回调方法会被触发执行。这样,我们网站的主线程不需要再为了监听元素相交而辛苦劳作,浏览器会自行优化元素相交管理。
用法
- 创建一个 intersection observer
● 创建一个 IntersectionObserver 对象,并传入相应参数和回调用函数,该回调函数将会在目标 (target) 元素和根 (root) 元素的交集大小超过阈值 (threshold) 规定的大小时候被执行。
let options = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 1.0
}
//回调函数只会在元素达到thresholds 规定的阈值时才会执行。
let observer = new IntersectionObserver(callback, options);
属性 | 说明 |
---|---|
root | 必须是目标元素的父级元素。如果未指定或者为null,则默认为浏览器视窗。 |
threshold | 目标 (target) 元素与根 (root) 元素之间的交叉比 (intersection ratio)取值在 0.0 和 1.0 之间。可以是单一的 number 也可以是 number 数组,默认值是 0 (意味着只要有一个 target 像素出现在 root 元素中,回调函数将会被执行)。该值为 1.0 含义是当 target 完全出现在 root 元素中时候 回调才会被执行。 |
rootMargin | 用来扩大或者缩小视窗的的大小 |
若rootMargin: ‘30px 100px 20px’ 如下图所示:
蓝线表示定义的root元素,添加rootMargin属性后,将原视窗增大,虚线就是现在的视窗,所以元素现在也就在视窗里面了。
let callback =(entries, observer) => {
entries.forEach(entry => {
// entry.boundingClientRect 返回包含目标元素的边界信息
//entry.rootBounds 返回根元素的边界信息
// entry.intersectionRatio. 返回intersectionRect 与 boundingClientRect 的比例值。
// entry.intersectionRect. 用来描述根和目标元素的相交区域。
// entry.isIntersecting
//返回一个布尔值,如果目标元素与根相交,则返回true. 如果返回false, 变换是从交叉状态到非交叉状态。
// entry.target 与根出现相交区域改变的元素
});
};
打印出来如下:
- 给定一个目标元素进行观察
let target = document.querySelector('#listItem');
observer.observe(target);
//unobserve() 停止监听特定目标元素
请留意,你注册的回调函数将会在主线程中被执行。所以该函数执行速度要尽可能的快。如果有一些耗时的操作需要执行,建议使用 Window.requestIdleCallback() 方法
- 停止监听特定目标元素
observer.unobserve(target);//例如图片懒加载时,加载完后即停止监听该元素
- IntersectionObserver对象停止全部监听工作
observer.disconnect();
Intersection Observer的缺点
- IntersectionObservers中的回调是在主线程中运行的,数据是异步传递的,这意味着我们的回调函数的调用优先级是比较低的,要等到浏览器空闲时才能执行。因此使用此api完成类似滚动相关的动画注定失败,因为数据在你使用它的时候已经过时了。
2.api只是告诉我们元素什么时候进入视窗中,但是它没告诉我们元素是否被页面其他内容挡住了,或者元素的可视display是否被改成了其他效果 (transform, opacity, filter, etc)。对于顶级文档中的元素,可以通过分析 javascript 中的 DOM 或通过 elementFromPoint() 来判断。 但是对于 iframe 中的元素,则束手无策
进阶:Intersection Observer V2 新增isVisible布尔字段,当该字段为true时可以保证元素一定可见,而没有被遮挡,并且它的options中新增两个字段:
trackVisibility:指示观察者是否将跟踪目标可见性的变化
delay:Set a minimum delay between notifications
3.Should I Use One IntersectionObserver Instance? Or One Per Element?
4.root的设置对结果有影响
threshhold的设置:https://javascript.tutorialink.com/intersectionobserver-does-not-work-on-small-screens-for-long-sections-js/
参考链接:
https://developer.mozilla.org/zh-CN/docs/Web/API/Intersection_Observer_API
https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect
https://web.dev/intersectionobserver-v2/
https://developer.chrome.com/blog/intersectionobserver/#what-is-intersectionobserver-not-about
https://www.w3.org/TR/intersection-observer/
https://www.bennadel.com/blog/3946-using-intersectionobserver-and-ngswitch-to-defer-template-bindings-in-angular-11-0-5.htm
https://github.com/w3c/IntersectionObserver/issues/81