背景
为了配合项目的一个前端曝光埋点功能,涉及到列表滚动,动态上报曝光行的数据,进行了一个技术调研。
在前端开发工作中,常常需要判断某个元素是否进入了“视口”,一般的做法是监听滚动容器的滚动事件,调用目标元素位置方法,一般有两种方式:
el.offesetTop - document.documentElement.scrollTop <= viewPortHeight
el.getBoundingClientReact().top <= viewPortHeight
第一种:
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
}
第二种:
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
}
但是这种会造成页面的重排,对性能影响很大。本身社招ATS列表已经非常臃肿,且已经有一些性能问题,所以需要寻找性能更好的API来实现。
常见引起重排属性和方法:
width height margin padding display border position overflow clientWidth clientHeight
clientTop clientLeft offsetWidth offsetHeight offsetTop offsetLeft scrollWidth scrollHeight scrollTop
scrollLeft scrollIntoView() scrollTo() getComputedStyle() getBoundingClientRect() scrollIntoViewIfNeeded()
介绍IntersectionObserver API
目前有一个新的 IntersectionObserver API
,可以自动"观察"元素是否可见,Chrome 51+
已经支持。由于可见(visible)
的本质是,目标元素与视口产生一个交叉区,所以这个API
叫做"交叉观察器"。
API
//初步用法
var io = new IntersectionObserver(callback, option);
IntersectionObserver 是浏览器原生提供的构造函数,接受两个参数: callback是可见性变化时的回调函数,option是配置对象
构造函数的返回值是一个观察器实例。实例的observe方法可以指定观察哪个 DOM 节点。
observe的参数是一个 DOM 节点对象。如果要观察多个节点,就要多次调用这个方法。
// 开始观察
io.observe(document.getElementById('example'));
// 停止观察
io.unobserve(element);
// 关闭观察器
io.disconnect();
callback
var io = new IntersectionObserver( entries => { console.log(entries); } );
entries
参数是一个数组,每个被观察的成员都是一个IntersectionObserverEntry
对象,比如观察了两个元素,那么entries
的长度就是2
注意: 第一次调用new IntersectionObserver
时,callback
函数会先调用一次,即使元素未进入可视区域。
IntersectionObserverEntry
IntersectionObserverEntry
对象提供目标元素的信息,一共有七个属性
time
:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
target
:被观察的目标元素,是一个 DOM
节点对象
rootBounds
:根元素的矩形区域的信息,getBoundingClientRect()
方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
boundingClientRect
:目标元素的矩形区域的信息
intersectionRect
:目标元素与视口(或根元素)的交叉区域的信息
intersectionRatio
:目标元素的可见比例,即intersectionRect占boundingClientRect
的比例,完全可见时为1,完全不可见时小于等于0
isIntersecting
:一个布尔值,指示target
元素是已转换为相交状态(true)
还是已脱离相交状态(false)
。
Option
IntersectionObserver
构造函数的第二个参数是一个配置对象。它可以设置以下属性。
threshold
threshold
属性决定了什么时候触发回调函数。它是一个数组,每个成员都是一个门槛值,默认为[0]
,即交叉比例(intersectionRatio)
达到0
时(开始出现、完全隐藏)触发回调函数。
比如threshold
设置为[0.5]
,这个时候当元素第一次进入视口50%
本身的面积的时候会调用一次,最后滚动出视口 50%
本身的面积的时候会再调用一次。
这个是为啥?
因为没有取消observer
,如果说只想要调用一次,只要在触发之后调用 unobserve
方法取消 observer
就可以了。
new IntersectionObserver(
entries => {/* ... */},
{
threshold: [0, 0.25, 0.5, 0.75, 1]
});
用户可以自定义这个数组。比如,[0, 0.25, 0.5, 0.75, 1]
就表示当目标元素0%、25%、50%、75%、100%
可见时,会触发回调函数。
root、rootMargin
很多时候,目标元素不仅会随着窗口滚动,还会在容器里面滚动(比如在iframe
窗口里滚动)。容器内滚动也会影响目标元素的可见性
IntersectionObserver API
支持容器内滚动。root属性指定目标元素所在的容器节点(即根元素)。注意,容器元素必须是目标元素的祖先节点。
大白话来说,root
其实就是可以设置以哪个滚动元素为标准,进行视口的计算。
var opts = {
root: document.querySelector('.container'),
rootMargin: "500px 0px"
};
var observer = new IntersectionObserver(
callback,
opts
);
上面代码中,除了root
属性,还有rootMargin
属性。后者定义根元素的margin
,用来扩展或缩小rootBounds
这个矩形的大小,从而影响intersectionRect
交叉区域的大小。它使用CSS
的定义方法,比如10px 20px 30px 40px
,表示 top、right、bottom
和 left
四个方向的值。 这样设置以后,不管是窗口滚动或者容器内滚动,只要目标元素可见性变化,都会触发观察器。
注意点
IntersectionObserver API
是异步的,不随着目标元素的滚动同步触发。IntersectionObserver
的实现,应该采用requestIdleCallback()
,即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。
关于 requestIdleCallback
可参考: https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
兼容性
https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver
polyfill
https://www.npmjs.com/package/intersection-observer
实践
实例一:图片懒加载
var url = ''; // 图片链接
var io = new IntersectionObserver(
entries => {
console.log(entries);
entries.forEach(item => {
if(item.isIntersecting) {
// 正常的图片懒加载设计应该是默认加载一张缩略图,然后等滚动到视口内再替换 src ,这里直接用 innerHTML 方便,只是演示使用
item.target.innerHTML = `<img height="100%" src="${url}" />`;
io.unobserve(item.target)
}
})
}, {
threshold: 1,
root: document.querySelector('.container'),
});
const boxList = document.querySelectorAll('.observer-item');
boxList.forEach(item => {
io.observe(item);
})
实例二:无限滚动
var io = new IntersectionObserver(
entries => {
console.log(entries);
entries.forEach(item => {
if(item.isIntersecting) {
loadItems();
}
})
}, {
threshold: 0.3,
root: document.querySelector('.container'),
});
io.observe(document.querySelector('.observer-target'));
function loadItems(){ /*加载新的items*/}
无限滚动时,最好在页面底部有一个页尾栏(又称sentinels
)。一旦页尾栏可见,就表示用户到达了页面底部,从而加载新的条目放在页尾栏前面。这样做的好处是,不需要再一次调用observe()
方法,现有的IntersectionObserver
可以保持使用。
实例三:埋点曝光
const boxList = document.querySelectorAll('.mt-table-scroll .mt-table-row');
if(boxList.length <= 0) return;
var io = new IntersectionObserver((entries) =>{
entries.forEach(item => {
// 。。。 埋点曝光代码
if (item.isIntersecting) {
// do something
}
io.unobserve(item.target)
}
})
}, {
root: null,
threshold: 0.05, // 阀值设为0.05,当只有比例达到0.05时才触发回调函数
})
// observe遍历监听所有box节点
boxList.forEach(box => io.observe(box))