JS63 IntersectionObserver API

mac2024-06-03  30

元素可见性

页面的可见性可以用document.visibilityState或者document.hidden获得,通过document.visibilitychange来监听页面可见性的变化,但是对于页面的元素的可见性却只能手动通过位置判断。

例如下面这个例子

需要监听scroll事件,根据target元素的是否出现,来更改顶部的文字和样式。由于target内嵌了几层,所以判断时需要通过getBoundingClientRect方法获得其位置,与视口位置比较,比较的标的还需要根据其父元素的位置变化,实现起来还是有一点麻烦的:

<div class="container"> <header class="head" id="head"></header> <main class="main" id="main"> <div class="content"> <div class="target-container" id="targetContainer"> <div class="target" id="target"> </div> </div> </div> </main> <footer class="foot" id="foot"></footer> </div> <script> window.onload = () => { const head = document.querySelector('#head'), foot = document.querySelector('#foot'), main = document.querySelector('#main'), targetContainer = document.querySelector('#targetContainer'), target = document.querySelector('#target'); const footTop = foot.getBoundingClientRect().top; // 改变 head 的文字和样式 const changeHeadState = isVisible => { head.textContent = isVisible ? 'Visible' : 'Invisible'; head.setAttribute('style', `background: ${isVisible ? 'lightgreen' : 'red'}`) }; const watchPosition = () => { const targetContainerRect = targetContainer.getBoundingClientRect(), targetContainerBottom = targetContainerRect.top + targetContainerRect.height; const targetTop = target.getBoundingClientRect().top; let isVisible = false; // targetContainer 完全可见 if (targetContainerBottom < footTop) { isVisible = targetTop < targetContainerBottom; } else { isVisible = targetTop < footTop; } return isVisible }; changeHeadState(watchPosition()); main.addEventListener('scroll', () => { changeHeadState(watchPosition()); }); targetContainer.addEventListener('scroll', () => { changeHeadState(watchPosition()); }); } </script>

IntersectionObserver API

现在有了一个新的API来帮助我们简化这个工作,那就是IntersectionObserver API,它的兼容性如下:

使用的时候首先需要新建一个实例:

const io = new IntersectionObserver(callback, option);

其中callback是监听元素的可见性发生变化时的回调函数,它会在元素进入视口(开始可见)或者完全离开视口(开始不可见)时被触发,它的参数是一个数组,每一项都是一个监听对象对应的IntersectionObserverEntry对象,它主要包含以下属性:

time,可见性发生变化的时间,是一个高精度时间戳,单位为毫秒target,被观察的目标元素,DOM节点rootBounds,根元素的getBoundingClientRect()的返回值boundingClientRect,目标元素的getBoundingClientRect()的返回值intersectionRect,目标元素与视口(或根元素)交叉区域getBoundingClientRect()的返回值intersectionRatio,目标元素的可见比例,即intersectionRect占boundingClientRect的比例,完全可见时为1,完全不可见时小于等于0

在Chrome 78版本中会返回isVisible属性,但是不知道是不是Bug,无论元素是否可见,都为false,但是isTntersecting的表现是正常的,所以判断是否可见,可以根据intersectionRatio或者isTntersecting来进行判断

构造函数的第二个参数option是一个配置对象,可以配置的属性包括root、rootMargin、threshold,具体配置方法参考文档。

通过构造函数新建了一个观察器实例,实例的observe方法可以观察传入的DOM节点:

// 开始观察 io.observe(document.getElementById('example')); // 停止观察 io.unobserve(element); // 关闭观察器 io.disconnect();

如果要观察多个对象,就需要多次调用observe方法。

注意,IntersectionObserver API是异步的,不随着目标元素的滚动同步触发。IntersectionObserver的实现,应该采用requestIdleCallback(),即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。

Demo

可以使用IntersectionObserver API重写上面的例子,简化判断元素可见的逻辑:

window.onload = () => { const head = document.querySelector('#head'), target = document.querySelector('#target'); // 改变 head 的文字和样式 const changeHeadState = isVisible => { head.textContent = isVisible ? 'Visible' : 'Invisible'; head.setAttribute('style', `background: ${isVisible ? 'lightgreen' : 'red'}`) }; const io = new IntersectionObserver(([obj]) => { console.log(obj); const isVisible = obj.intersectionRatio > 0; changeHeadState(isVisible) }); io.observe(target); }

简单的无线滚动

可以使用这个API,实现一个简单的无线滚动的Demo

<div class="container"> <header class="head" id="head"></header> <main class="main" id="main"> <ul class="content" id="content"> </ul> <div class="target" id="target"></div> </main> <footer class="foot" id="foot"></footer> </div> <script> window.onload = () => { const content = document.querySelector('#content'), head = document.querySelector('#head'), target = document.querySelector('#target'); // 模拟网络请求 const fetch = () => { return new Promise(resolve => { setTimeout(resolve, 2000, 10) }) }; // 改变 head 的文字和样式 const getNewContent = (() => { // 标志服,避免重复请求 let pending = false; const frag = document.createDocumentFragment(); return isVisible => { if (!isVisible) { return; } if (pending) { return; } pending = true; head.textContent = 'Loading...'; fetch().then(num => { for (let i = 0; i < num; i++) { const li = document.createElement('li'); li.classList.add('item'); frag.appendChild(li) } content.appendChild(frag); pending = false; head.textContent = 'Done!'; }) }; })(); const io = new IntersectionObserver(([obj]) => { const isVisible = obj.intersectionRatio > 0; getNewContent(isVisible) }); io.observe(target); } </script>

在列表的末尾放置一个标志元素,当它可见的时候,就去触发网络请求,获取新的数据。

参考

IntersectionObserver API 使用教程@阮一峰的网络日志Intersection Observer@MDNIntersectionObserverEntry@MDN
最新回复(0)