在一些场景下,尤其是在移动端,我们需要判断元素是否进入用户视口(viewport),来进行以下处理,例如无限滚动,当底部元素进入视口之后加载下一页内容。
通常的做法是监听 scroll 事件,然后调用目标元素的 getBoundingClientRect 方法获取元素位置,判断其是否出现在视口之内,缺点是 scroll 触发非常密集,导致计算量很大。
现在新出的 IntersectionObserver 可以自动观察元素是否可见,截止 2022.07.31 其浏览器的适配情况如下
那么接下来我们就来看一下它的具体语法。
构造器
var observer = new IntersectionObserver(callback[, options]);
构造器返回一个 IntersectionObserver 对象
如果指定
rootMargin
则会检查其是否符合语法规定,检查阈值以确保全部在 0.0 到 1.0 之间,并且阈值列表会按升序排列。如果阈值列表为空,则默认为一个 [0.0] 的数组。
- callback:当元素可见比例超过指定阈值后,会调用一个回调函数,此回调函数接受两个参数,进入视口和离开视口都会触发
- entries:IntersectionObserverEntry列表,。
- observer:被调用的IntersectionObserver实例。
- options:用来配置 observer 实例的对象,如果 options 未指定,observer 实例默认使用文档视口作为 root,并且没有 margin,阈值为 0%
- root:监听元素的祖先元素 Element 对象,其边界盒将被视作视口。
- rootMargin:一个在计算交叉值时添加至根的边界盒中的一组偏移量,默认值是"0px 0px 0px 0px"。
- threshold:规定了一个监听目标与边界盒交叉区域的比例值,可以是一个具体的数值或是一组 0.0 到 1.0 之间的数组。
当rootMargin不存在时会抛出语法错误,SyntaxError
一个或多个阈值超出了 0.0 到 1.0 的范围时会抛出返回错误,RangeError
方法
同其他 Observer API 一样,IntersectionObserver 也提供了那几个方法,下面我们将按照这套模板来演示各个 API 的效果
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> #container { width: 200px; height: 300px; overflow: auto; background-color: #fff; } .content { height: 200px; } .content:nth-of-type(odd) { background-color: pink; } .content:nth-of-type(even) { background-color: blue; } #footer { background-color: red; height: 30px; } </style> </head> <body> <div id="container"> <div class="content"></div> <div class="content"></div> <div id="footer">到底了</div> </div> <script src="./index.js"></script> </body> </html> 复制代码
observe
添加监听元素,当元素出现或消失在视口中时触发 observer 的回调
const footer = document.getElementById('footer'); const observer = new IntersectionObserver((entries) => { console.log(entries); }) observer.observe(footer) 复制代码
在页面上进行滚动时就能看到效果,如果是显示触发的,isIntersecting 值为 true,如果是隐藏触发的,isIntersecting 的值就是 false
unobserve
observer.unobserve(document.getElementById('footer')) 复制代码
取消观察后滚动元素不再触发回调
takeRecords
返回一个IntersectionObserverEntry
对象,每个对象的目标都包含每次相交的信息,可以通过调用此地地观察者的方法显式或隐式调用。
Note: 如果使用回调来监视这些更改,则无需调用此方法。调用此方法会清除挂起的相交状态列表,因此不会运行回调。
const intersectionObserverEntries = intersectionObserver.takeRecords(); 复制代码
disconnect
停止当前 observer 对所有元素的监听
observer.disconnect() 复制代码
应用场景
懒加载
一些元素,如图片等,在 DOM 渲染阶段可以不加载,当图片出现在视口时开始加载
// 省略 20 个 img 元素 const observer = new IntersectionObserver( (changes) => { console.log(changes); changes.forEach(({ target, isIntersecting }) => { if (!target.src && isIntersecting) target.src = target.dataset.src }); } ); Array.from(document.querySelectorAll('img')) .forEach((item) => { observer.observe(item); }); 复制代码
我们通过触发监听器的元素解构获得 target 元素和 isIntersecting 属性,通过元素是否存在 src 属性和是否进入视口来处理图片
最终效果如下
无限滚动
当靠近列表底部时,开始加载下一页内容
const container = document.getElementById('container'); // ul元素 const footer = document.getElementById('footer'); // div let id = 1 // 递增 id const observer = new IntersectionObserver((entries) => { entries.forEach(({ isIntersecting }) => { if (isIntersecting) { footer.innerText = 'loading...' setTimeout(() => { const newList = [] for (let i = 0; i < 20; i++) { const li = document.createElement('li') li.appendChild(document.createTextNode(id++)) container.appendChild(li) } footer.innerText = '到底了' }, 2000) } }) }) observer.observe(footer) 复制代码
监听 footer 元素,当 footer 元素出现在视口中时开始请求数据,这里用定时器进行模拟,将新的元素添加到无需列表中。
效果如下:
IntersectionObserverEntry
time
:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒target
:被观察的目标元素,是一个 DOM 节点对象rootBounds
:根元素的矩形区域的信息,getBoundingClientRect()
方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
boundingClientRect
:目标元素的矩形区域的信息intersectionRect
:目标元素与视口(或根元素)的交叉区域的信息intersectionRatio
:目标元素的可见比例,即intersectionRect
占boundingClientRect
的比例,完全可见时为1
,完全不可见时小于等于0
isIntersecting
:布尔值,表示目标元素的相交状态变化,true 表示从不相交到相交(即出现在视口),false 反之