判断是否在可视范围内

埋点
最近项目中需要对用户行为进行分析,进入页面时触发,点击触发以及模块曝光时触发
手动埋点,页面访问埋点可以对路由进行封装,在进入当前页面时自动上报,点击触发可以监听元素的点击事件,在不影响原有的代码之前,模块曝光需要监听当前dom元素是否在可视范围内,使用IntersectionObserver观察者对象。
埋点其实也是一种前端监控方式,收集用户的行为习惯,能更好的发现前端项目问题,分析白屏事件,不同用户,不同机型和不同系统下的首屏加载时间等等。
IntersectionObserver
用途:埋点、无限滚动、图片懒加载、控制动画/视频执行(性能优化)
使用场景:判断某个元素是否进入了视口,传统的实现方法是,监听scroll事件,调用目标元素的getBoundingClientRect()方法,得到对于视口左上角的位置,再去判断是否在视口之内,缺点是由于scroll事件频发,计算量很大,容易造成性能问题
IntersectionObserver浏览器提供的原生方法,可以自动观察元素是否可见
用法
以new的形式声明一个对象,接收两个参数callback和options
callback是添加监听后,当监听目标发生滚动时触发的回调函数。接收一个参数entries,即IntersectionObserverEntry实例,描述了目标元素与root的交叉状态。
|属性|说明| |-|-| |boundingClientRect|返回包含目标元素的边界信息,返回结果与element.getBoundingClientRect() 相同| |intersectionRatio|返回目标元素出现在可视区的比例| |intersectionRect|用来描述root和目标元素的相交区域| |isIntersecting|返回一个布尔值,下列两种操作均会触发callback:1. 如果目标元素出现在root可视区,返回true。2. 如果从root可视区消失,返回false| |rootBounds|用来描述交叉区域观察者(intersection observer)中的根.| |target|目标元素:与根出现相交区域改变的元素 (Element)| |time|返回一个记录从 IntersectionObserver 的时间原点到交叉被触发的时间的时间戳|
表格中加粗的两个属性是比较常用的判断条件:isIntersecting和intersectionRatio
options
options是一个对象,用来配置参数,也可以不填。
|属性|说明| |-|-| |root|所监听对象的具体祖先元素。如果未传入值或值为null,则默认使用顶级文档的视窗(一般为html)。| |rootMargin|计算交叉时添加到根(root)边界盒bounding box的矩形偏移量, 可以有效的缩小或扩大根的判定范围从而满足计算需要。所有的偏移量均可用像素(px)或百分比(%)来表达, 默认值为"0px 0px 0px 0px"。| |threshold|一个包含阈值的列表, 按升序排列, 列表中的每个阈值都是监听对象的交叉区域与边界区域的比率。当监听对象的任何阈值被越过时,都会触发callback。默认值为0。|
方法
|方法|说明| |-|-| |observe()|开始监听一个目标元素| |unobserve()|停止监听特定目标元素| |takeRecords()|返回所有观察目标的IntersectionObserverEntry对象数组| |disconnect()|使IntersectionObserver对象停止全部监听工作|
埋点hooks
import { useState, useCallback, useEffect } from 'react'
/**
* UseIntersectionObserver
* @param observerList 由被观察目标所组成的数组,数组项是由React.createRef构建出来的对象
* @param callback 当目标元素被曝光所需要触发的函数,该函数接受一个参数indexList,由被曝光元素在observerList数组中的索引组成
* @param infinite 是否持续观察目标元素,默认值为false。(因为曝光打点一般只需上报一次)
* @param opt 可以自定义曝光条件(值的构成参考MDN),默认为{ threshold: [1] },只有当目标元素完全暴露在可视区内才触发回调
* @param deps 依赖项
*/
function UseIntersectionObserver (
observerList,
callback,
infinite,
opt,
deps
) {
// list 为需要监听的元素列表。setList做为UseIntersectionObserver函数的返回值,可以让调用者修改需要监听的 list
const [list, setList] = useState(observerList)
// intersectionObserver: 观察者对象
let intersectionObserver = null
const observeExposure = useCallback((list) => {
if (typeof IntersectionObserver === 'undefined') {
throw new Error('Current browser does not support IntersectionObserver ')
}
if (!list || list.length === 0) return
// 当观察者存在时销毁该对象
intersectionObserver && intersectionObserver.disconnect()
// 构造新的观察者实例
intersectionObserver = new IntersectionObserver(entries => {
// 保存本次监听被曝光的元素
let activeList = []
// 递归每一个本次被监听对象,如果按照曝光条件出现在可视区,则调用callback函数,并且取消监听
entries.forEach(entrie => {
// 找出本次被监听对象在list中的索引
const index = Array.from(list).findIndex(
item => item.current === entrie.target
)
// 防止意外发生
if (index === -1) return
// isIntersecting是每个被监听的元素所自带的属性,若为ture,则表示被曝光
// 并且未被曝光过
if (entrie.isIntersecting) {
// 保存本次曝光元素索引
activeList.push({index, target: entrie.target})
// 解除观察, 若需要无限观察则不取消监听
!infinite &&
intersectionObserver &&
intersectionObserver.unobserve(list[index].current)
}
})
// callback函数
activeList.length > 0 && callback(activeList)
}, opt)
list.forEach(item => {
item.current &&
intersectionObserver &&
intersectionObserver.observe(item.current)
// 可以兼容直接传入DOM节点。
// if((<React.RefObject<any>>item).current) {
// intersectionObserver.observe((<React.RefObject<any>>item).current)
// } else if ((<HTMLElement>item)) {
// intersectionObserver.observe((<HTMLElement>item))
// }
})
}, [deps])
useEffect(() => {
observeExposure(list)
// 当 umount 时取消链接
return () => {
intersectionObserver && intersectionObserver.disconnect()
}
}, [list])
return [setList]
}
const useIntersectionObserver = UseIntersectionObserver
export default useIntersectionObserver
图片懒加载
原理:
- 图片是否进入可视区域 intersectionRatio
- 将图片的具体地址暂存到data-src属性
- 图片进入可视区后,将img标签的data-src属性赋值给src属性
// ref存储可变值
const observerRef = useRef(new IntersectionObserver((entries) => {
entries.forEach((entry) => {
const { target, intersectionRatio } = entry;
// target 目标dom
// intersectionRatio 相交区域和目标元素的比例值,进入可视区域大于0 否则等于0
// isIntersecting 是否进入可视区域
if (intersectionRatio > 0) {
target.src = target.dataset.src; // 替换目标元素的src路径
target.onload = () => {
target.style.opacity = '1'
}
observerRef.current.unobserve(target); // 关闭监听目标元素
}
})
}))
useEffect(() => {
// // 创建观察者实例
// let observer = new IntersectionObserver((entries, observer) => {
// entries.forEach(entry => {
// let target = entry.target; // 目标元素
// // 当元素出现在视图中
// if (entry.intersectionRatio > 0) {
// target.src = target.dataset.src; // 替换元素的src路径
// observer.unobserve(target) // 关闭监听目标元素
// }
// })
// })
Array.from(document.querySelectorAll('img')).forEach(image => {
observerRef.current.observe(image); // 监听每一个元素
})
return () => {
observerRef.current.disconnect(); // 取消所有监听dom
}
}, []);