使用Intersection Observer API 检测元素是否出现在可视窗口
使用Intersection Observer API 检测元素是否出现在可视窗口
API解读:
Intersection Observer API提供了一种异步检测目标元素与祖先元素或视口(可统称为根元素)相交情况变化的方法。
注意点:因为该 API 是异步的,它不会随着目标元素的滚动同步触发,而IntersectionObserver API是通过requestIdleCallback()实现,即只有浏览器空闲下来,才会执行观察器。
Intersection observer 的重要概念
Intersection observer API 有以下五个重要的概念:
- 目标(target)元素 — 我们要监听的元素
- 根(root)元素 — 帮助我们判断目标元素是否符合条件的元素
- 以下两种情况根元素会默认为顶级文档的视口(一般为 html)。
-
- 目标元素不是可滚动元素的后代且不传值时
-
- 指定根元素为 null
- 交叉比(intersection ratio)—目标元素与根根的交集相对于目标元素百分比的表示(取值范围 0.0-1.0)。
- 阈值(threshold) — 回调函数触发的条件。
- 回调函数(callback) — 为该 API 配置的函数,会在设定的条件下触发。
用法
是以new
的形式声明一个对象,接收两个参数callback
和options
const io = new IntersectionObserver(callback [,options])
io.observe(DOM)
callback
callback是添加监听后,当监听目标发生滚动变化时触发的回调函数。接收一个参数entries,即IntersectionObserverEntry实例。描述了目标元素与root的交叉状态。具体参数如下:
属性 | 说明 |
---|---|
boundingClientRect | 返回包含目标元素的边界信息,返回结果与element.getBoundingClientRect() 相同 |
intersectionRatio | 返回目标元素出现在可视区的比例 |
intersectionRect | 用来描述root和目标元素的相交区域 |
isIntersecting | 返回一个布尔值,下列两种操作均会触发callback:1. 如果目标元素出现在root可视区,返回true。2. 如果从root可视区消失,返回false |
rootBounds | 用来描述交叉区域观察者(intersection observer)中的根. |
target | 目标元素:与根出现相交区域改变的元素 (Element) |
time | 返回一个记录从 IntersectionObserver 的时间原点到交叉被触发的时间的时间戳 |
表格中加粗的两个属性是比较常用的判断条件:isIntersecting(是否出现在可视区)和intersectionRatio(出现在可视区的比例)
options
options是一个对象,用来配置参数,也可以不填。共有三个属性,具体如下:
属性 | 说明 |
---|---|
root | 所监听对象的具体祖先元素。如果未传入值或值为null,则默认使用顶级文档的视窗(一般为html)。 |
rootMargin | 计算交叉时添加到根(root)边界盒bounding box的矩形偏移量, 可以有效的缩小或扩大根的判定范围从而满足计算需要。所有的偏移量均可用像素(px)或百分比(%)来表达, 默认值为"0px 0px 0px 0px"。 |
threshold | 一个包含阈值的列表, 按升序排列, 列表中的每个阈值都是监听对象的交叉区域与边界区域的比率。当监听对象的任何阈值被越过时,都会触发callback。默认值为0。 |
方法
介绍了这么多配置项及参数,差点忘了最重要的,IntersectionObserver有哪些方法? 如果要监听某些元素,则必须要对该元素执行一下observe
方法 | 说明 |
---|---|
observe() | 开始监听一个目标元素 |
unobserve() | 停止监听特定目标元素 |
takeRecords() | 返回所有观察目标的IntersectionObserverEntry对象数组 |
disconnect() | 使IntersectionObserver对象停止全部监听工作 |
实际应用
- 图片懒加载
const imgList = [...document.querySelectorAll('img')]
var io = new IntersectionObserver((entries) =>{
entries.forEach(item => {
// isIntersecting是一个Boolean值,判断目标元素当前是否可见
if (item.isIntersecting) {
item.target.src = item.target.dataset.src
// 图片加载后即停止监听该元素
io.unobserve(item.target)
}
})
})
// observe遍历监听所有img节点
imgList.forEach(img => io.observe(img))
- 埋点曝光
假如有个需求,对一个页面中的特定元素,只有在其完全显示在可视区内时进行埋点曝光。
const boxList = [...document.querySelectorAll('li')]
var io = new IntersectionObserver((entries) =>{
entries.forEach(item => {
// intersectionRatio === 1说明该元素完全暴露出来,符合业务需求
if (item.intersectionRatio === 1) {
// 。。。 埋点曝光代码
// do something...
io.unobserve(item.target)
}
})
}, {
root: null,
threshold: 1, // 阀值设为1,当只有比例达到1时才触发回调函数
})
// observe遍历监听所有box节点
boxList.forEach(item => io.observe(item))
- demo:
大家可以在自己电脑运行一下下面的代码,会有更深的理解。
<!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>
li {
width: 200px;
height: 400px;
border: 1px solid gray;
}
</style>
</head>
<body>
<ul>
<li>1-aaa</li>
<li>2-bbb</li>
<li>3i-ccc</li>
<li>4i</li>
<li>5i</li>
<li>6i</li>
<li>7i</li>
<li>8i</li>
<li>9i</li>
<li>10i</li>
<li>l1</li>
<li>l2</li>
<li>l3</li>
<li>l4</li>
<li>l5</li>
<li>l6</li>
<li>l7</li>
<li>l8</li>
<li>l9</li>
<li>10</li>
<li>21</li>
<li>22</li>
<li>23</li>
<li>24</li>
<li>25</li>
<li>26</li>
<li>27</li>
<li>28</li>
<li>29</li>
<li>30</li>
</ul>
<script>
const imgList = [...document.querySelectorAll("li")];
const options = {
root: null,
rootMargin: "1px",
thresholds: 1,
};
//io 为 IntersectionObserver对象 - 由IntersectionObserver()构造器创建
const io = new IntersectionObserver((entries) => {
//entries 为 IntersectionObserverEntry对像数组
entries.forEach((item) => {
//item 为 IntersectionObserverEntry对像
// item.isIntersecting是一个Boolean值,判断目标元素当前是否可见
if (item.isIntersecting) {
console.log(item);
// item.target.src = item.target.dataset.src;
// li加载后即停止监听该元素
io.unobserve(item.target);
}
// intersectionRatio === 1说明该元素完全暴露出来
// if (item.intersectionRatio === 1) {
// 埋点曝光代码
// do something...
// 停止监听该元素
// io.unobserve(item.target);
// }
});
}, options); //不传options参数,默认根元素为浏览器视口
// observe遍历监听所有li节点
imgList.forEach((li) => io.observe(li));
</script>
</body>
</html>
关于如何判断元素是否在可视区域内的其他方法
第一种方法:offsetTop、scrollTop
公式: el.offsetTop - document.documentElement.scrollTop <= viewPortHeight
function isInViewPortOfOne (el) {
// viewPortHeight 兼容所有浏览器写法
const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
const offsetTop = el.offsetTop
const scrollTop = document.documentElement.scrollTop
const top = offsetTop - scrollTop
console.log('top', top)
// 这里有个+100是为了提前加载+ 100
return top <= viewPortHeight + 100
}
第二种方法:getBoundingClientRect
- 返回值是一个 DOMRect 对象,拥有 left, top, right, bottom, x, y, width, 和 height 属性
公式: el.getBoundingClientReact().top <= viewPortHeight
其实, el.offsetTop - document.documentElement.scrollTop = el.getBoundingClientRect().top, 利用这点,我们可以用下面代码代替方法一
function isInViewPortOfTwo (el) {
const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
const top = el.getBoundingClientRect() && el.getBoundingClientRect().top
console.log('top', top)
return top <= viewPortHeight + 100
}