Intersection Observer API 学习笔记。
元素可见性
页面的可见性可以用document.visibilityState或者document.hidden获得,通过document.visibilitychange来监听页面可见性的变化,但是对于页面的元素的可见性却只能手动通过位置判断。
例如下面这个例子

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

使用的时候首先需要新建一个实例:
1
| 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节点:
1 2 3 4 5 6 7 8
| io.observe(document.getElementById('example'));
io.unobserve(element);
io.disconnect();
|
如果要观察多个对象,就需要多次调用observe方法。
注意,IntersectionObserver API是异步的,不随着目标元素的滚动同步触发。IntersectionObserver的实现,应该采用requestIdleCallback(),即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。
Demo
可以使用IntersectionObserver API重写上面的例子,简化判断元素可见的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| window.onload = () => { const head = document.querySelector('#head'), target = document.querySelector('#target');
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

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
| <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) }) };
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>
|
在列表的末尾放置一个标志元素,当它可见的时候,就去触发网络请求,获取新的数据。
参考