J'Blog

判断是否在可视范围内

判断是否在可视范围内

埋点

最近项目中需要对用户行为进行分析,进入页面时触发,点击触发以及模块曝光时触发

手动埋点,页面访问埋点可以对路由进行封装,在进入当前页面时自动上报,点击触发可以监听元素的点击事件,在不影响原有的代码之前,模块曝光需要监听当前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
   }
 }, []);

ahooks

ahooks中也有类似相关的hooks:useInViewport也是基于intersectionObserve API