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