使用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的形式声明一个对象,接收两个参数callbackoptions

1
2
3
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对象停止全部监听工作

实际应用

  • 图片懒加载
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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))
  • 埋点曝光

假如有个需求,对一个页面中的特定元素,只有在其完全显示在可视区内时进行埋点曝光。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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:

大家可以在自己电脑运行一下下面的代码,会有更深的理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<!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

1
2
3
4
5
6
7
8
9
10
11
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, 利用这点,我们可以用下面代码代替方法一

1
2
3
4
5
6
7
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
}