上周接到业务侧的一个数据埋点上报的需求,要求上报内容模块的曝光。本以为只是举手之劳的事情,结果却因此发现了新姿势。
一般情况下,我们直接在组件初始化的时候直接上报,例如在React的函数组件:
useEffect(() => {
stat('PV')
}, [])
但这次有点不一样,该内容模块需要滚动页面才能被看到,业务侧想要更加精准的在内容模块出现在窗口时才上报曝光事件。
第一反应
既然需要滚动才能看到,那自然而然的想法就是,监听窗口的scroll
事件,并计算该内容模块是否出现在浏览器的视口(viewport)内,是的话就进行数据上报。
但这个方案有一定的性能问题,每次页面滚动都执行回调函数进行判断,如果页面上多个内容模块都需要做类似的数据上报的话,那就有点刺激了。更有甚者,如果页面有部分内容是可收缩展开,那意味着还得动态计算各个内容模块的位置。
更优雅的方案
这时候,一个叫IntersectionObserver
的api出现了,稍稍看了 MDN - IntersectionObserver 官方文档 就有眼前一亮的感觉。
它是一个用于观察指定元素在祖先元素/文档视口的显示状态的方法。它提供边界元素、边界元素偏移量、触发监听阈值等3个可自定义参数。
回到我的需求,我只需要初始化一个IntersectionObserver,指定观察监听的DOM元素(也就是我需要进行曝光上报的内容模块),并处理回调函数逻辑进行上报,最后停止监听(保证只上报1次)即可。
// 内容模块曝光上报
const viewObsvr = new IntersectionObserver(els => {
// intersectionRatio小于等于零,表示不在指定视野区域内
if (els[0].intersectionRatio <= 0) {
return
}
console.log('内容模块曝光')
viewObsvr.disconnect()
})
// 指定观察DOM元素
viewObsvr.observe(document.querySelector('#mybox'))
更多的使用场景
由此想到,还有很多的使用场景可以适用 IntersectionObserver
作为解决方案。(特别是通过监听窗口的scroll
事件的场景)
悬浮栏
很多网页都会在滚动浏览的时候,出现一个悬浮工具栏,方便用户进行跳转到指定章节、回到顶部、分享等操作。这时我们可以指定一个DOM元素,例如下面代码中的页头,并定义了偏移量200px,当向下偏移这个页头元素200px时,才会触发监听事件。
// 悬浮栏
const navElm = document.querySelector('#nav')
const headObsvr = new IntersectionObserver(els => {
if (els[0].intersectionRatio > 0) {
console.log('悬浮栏:隐藏')
navElm.style.display = 'none'
return
}
console.log('悬浮栏:显示')
navElm.style.display = 'block'
}, {
rootMargin: '200px 0px 0px 0px'
})
headObsvr.observe(document.querySelector('#header'))
列表下拉加载
列表滚动/下拉加载也是一个非常常规的功能,如果通过scroll
事件监听,就需要不断获取列表高度和判断滚动距离,为了避免事件的频繁触发,还需要引入截流和防抖逻辑代码。而IntersectionObserver
则只会在出现的时候触发一次,代码也非常简洁直观,简直就是程序员心中的白月光。
// 列表下拉加载
let i = 1
const loadingObsvr = new IntersectionObserver(els => {
console.log(els[0].intersectionRatio)
if (els[0].intersectionRatio <= 0) {
return
}
console.log('加载提示出现')
// 请求数据逻辑
// TODO:
})
loadingObsvr.observe(document.querySelector('#loading'))
背景色变化
这是一个有点花里胡哨的场景,灵感来源于intersectionRatio
表示指定DOM展示内容的百分比,那是不是一些CSS的展示属性和动画,可以通过这个数据动态控制,这里以background-color
为例。
// 背景色变化
const colorElm = document.querySelector('#colorBox')
const colorObsvr = new IntersectionObserver(els => {
colorElm.style.backgroundColor = `rgba(${240 - 120*els[0].intersectionRatio}, ${240 - 100*els[0].intersectionRatio}, ${2240 - 50*els[0].intersectionRatio}, 1)`
}, {
threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
})
colorObsvr.observe(colorElm)
兼容性
这个不算常用的api,其实早在Chrome 58就已经实现,目前在各大浏览器都已经广泛支持,甚至连移动端的都支持的非常好。