面试官:怎么判断元素是否在可视区域内?
我:使用offsetTop - scrollTop,如果得到的值小于等于clientHeight,就是在可视区域内
面试官:还有其他方法吗?
我:还可以通过getBoundingClientRect方法获取元素的位置进行判断
面试官:还有吗?
我:暂时只了解到这两种
面试官:回去再好好了解其他方法...
不管是图片懒加载、无限滚动列表、VirtualScroll 虚拟滚动列表、滚动条滚动到指定元素触发动画效果等,都涉及到怎么判断元素是否在可视区域内这个问题。那么到底有几种方法?这些方法对比有哪些区别?
1. 元素距离页面顶部高度 offsetTop - 滚动条滚动高度 scrollTop <= 屏幕可视窗口高度 screenHeight
const dom = document.getElementById("");
window.onscroll = () => {
// 获取可视窗口的高度。
const clientHeight = document.body.clientHeight;
// 获取滚动条滚动的高度
const scrollTop = document.documentElement.scrollTop;
// 获取元素偏移的高度。就是距离可视窗口的偏移量。
const offsetTop = dom.offsetTop;
if (offsetTop - scrollTop <= clientHeight) {
// 在可视区域内...
} else {
// 在可视区域外...
}
};
2. getBoundingClientRect
getBoundingClientRect 是 dom 对象的一个方法,该方法返回元素大小和它相对于视口的位置属性,位置属性分别为:
- top:元素上边框到视口顶端距离。
- left:元素左边框到视口左端距离。
- bottom:元素下边框到视口顶端距离。
- right:元素右边框到视口左端距离。
- width:元素的宽度(可选)。
- height:元素的高度(可选)。 那怎么判断子元素是否在可视区域内?答案是:top 大于等于 0 && left 大于等于 0 && bottom 小于等于屏幕可视窗口高度 && right 小于等于屏幕可视窗口高度
const dom = document.getElementById("");
window.onscroll = () => {
const clientHeight = document.body.clientHeight;
const clientWidth = document.body.clientWidth;
// 当滚动条滚动时,位置信息发生改变
const { top, right, bottom, left } = dom.getBoundingClientRect();
if (top >= 0 && left >= 0 && right <= clientWidth && bottom <= clientHeight) {
// 在可视区域内...
} else {
// 在可视区域外...
}
};
3. IntersectionObserver(交叉观察器)
IntersectionObserver 是一个用于观察目标元素与 root 根元素之间交叉状态变化的 API,并且是异步的,不随着目标元素的滚动同步触发。 它接收 2 个参数,第 1 个参数是 callback 回调函数,第 2 个参数是 options 配置对象。
3.1 callback 回调函数触发时机
- Observer 第一次监听目标元素的时候,也就是初始化时,触发回调函数
- 当目标元素进入或退出 root 根元素时,或者两个元素的相交部分大小发生变化时,触发回调函数
3.1.1 callback 回调函数接收两个参数:entries(返回目标元素的交叉信息)和 observer(观察者实例)
主要讲解一下 entries:它是一个数组,数组中包含了多个 IntersectionObserverEntry 实例,每个实例代表着一个目标元素与 root 根元素相交的信息。
- intersectionRatio 返回目标元素和 root 根元素相交区域占目标元素的比例值,范围是 0~1
- intersectionRect 返回一个 DOMRectReadOnly 对象,描述目标元素和 root 根元素相交区域的边界信息
- isIntersecting 返回目标元素和 root 根元素是否相交的布尔值
- rootBounds 返回一个 DOMRectReadOnly 对象,描述 root 根元素的边界信息
- target 目标元素,也就是当前 IntersectionObserverEntry 实例所对应的 DOM 元素
- time 相交时间的时间戳
3.2 options 配置对象
- root 指定根元素,以 dom 对象方式接收.它必须是目标元素的父级元素。如果未指定或者为 null,则默认为浏览器视窗。
- rootMargin 相当于 root 元素多了一个 margin 属性,如果没有这个 margin 属性,目标元素只有与 root 元素开始交叉时触发。而设置了 rootMargin 后,目标元素与 root 元素的外边距交叉时就会触发。默认值为四个边距全是 0。
- threshold root 根元素和目标元素相交程度达到 threshold 设置的值,回调函数被调用。threshold 可以是单一的 number 也可以是 number 数组。例如:设置为 0.5 时,root 根元素和目标元素相交程度达到 50%就触发回调函数; 设置为[0.25, 0.5]时,root 根元素和目标元素相交程度分别达到 25%和 50%时都会触发回调函数。
下面是我画的相交示例图(画的很丑,莫见怪...)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IntersectionObserver 示例</title>
<style>
* {
margin: 0;
padding: 0;
}
.content {
height: 2000px;
background-color: aquamarine;
}
#show-content {
height: 200px;
background: orange;
text-align: center;
}
#info-box {
width: 200px;
height: 200px;
line-height: 200px;
position: fixed;
top: 40%;
right: 50px;
background-color: bisque;
text-align: center;
}
</style>
</head>
<body>
<div class="content"></div>
<div id="show-content">我出现啦</div>
<div class="content"></div>
<div id="info-box">相交了</div>
<script>
const options = {
root: null, // 设置为null或者未指定,默认为浏览器视窗
rootMargin: "0px",
threshold: 0.25, // 相交25%时会触发回调
};
const io = new IntersectionObserver((entries) => {
console.log("entries", entries);
// isIntersecting 如果root根元素与目标元素相交并且达到threshold设置的相交程度,则为true,反之为false
if (entries[0].isIntersecting) {
document.getElementById("info-box").innerHTML = "相交了";
} else {
document.getElementById("info-box").innerHTML = "离开了";
}
}, options);
const dom = document.getElementById("show-content");
io.observe(dom);
</script>
</body>
</html>
总结一下
1(dom.offsetTop和滚动条高度差 <= 视窗高度) 和 2(getBoundingClientRect) 都是需要监听滚动条滚动事件,并且需要频繁调用元素的位置方法来获取元素的边界信息。事件监听和调用元素的位置方法都是在主线程上运行的,所以频繁触发、调用可能会造成性能问题。如果页面有很多滚动时的动画效果,性能消耗更大,页面卡顿问题就比较明显。
所以在 2016 年初,chrome51 率先推出了 IntersectionObserver 这个 API 来解决以上问题。它是一个异步的实例,只有在浏览器空闲的状态下才会触发,如果浏览器当前的事件队列中,有一系列的回调函数正在等待处理,该方法是不会被执行到的,只有当浏览器的事件队列为空,浏览器在空闲的时候,才会执行该方法。但这个API到现在为止一些旧版本的浏览器兼容性存在问题,没有前两种兼容性那么好。