React Hooks 之 useIntersectionObserver

前言

本文使用React Hooks 构建一个监听DOM元素曝光的Custom Hooks,主要功能是监听DOM元素是否在规定内曝光,从而可以完成曝光打点。

过去我们监听DOM元素都是通过监听scroll事件来监听目标元素是否在可视区,这样我们需要获取目标元素的一些数据。这样似乎很麻烦,而通过 Intersection Observer 来完成监听,更加方便、友好。

Intersection Observer

MDN: IntersectionObserver接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。

其他相关概念可见 谈谈IntersectionObserver懒加载 、 IntersectionObserver API 使用教程

简单来说这个对象会观察目标元素,当目标元素与它的祖先元素或者可视区发生交叉时,会触发回调函数。

通过这个对象我们可以监听需要曝光打点的元素,更方便的实现打点。同时我们不需要监听scroll事件以及相对应的DOM操作,减少了性能的开销。

Intersection Observer 在 React 中的实践

为了能够在不同的场景中使用,我们可以将其封装成HOOKS,更方便的调用该方法

1. 定义 useIntersectionObserver 函数

// useIntersectionObserver.ts
// 定义参数函数类型以及返回值类型

import { useState, useCallback, useEffect } from 'react'

type NumberList = number[]
type ObserverList = Arrayany>>
type CallbackFunction = (indexList: NumberList) => void
type ResultType = [React.Dispatchany>[]>>]

function UseIntersectionObserver (
  observerList: ObserverList,
  callback: CallbackFunction,
  infinite: boolean = false,
  opt: IntersectionObserverInit = {}
): ResultType {
  // list 为需要监听的元素列表。setList做为UseIntersectionObserver函数的返回值,可以让调用者修改需要监听的 list 
  const [list, setList] = useState(observerList)

  // intersectionObserver: 观察者对象
  let intersectionObserver: IntersectionObserver | null = null
  
  // ...
  return [setList]
}

const useIntersectionObserver = UseIntersectionObserver

export default useIntersectionObserver
复制代码

UseIntersectionObserver 函数参数:

  • observerList: 由被观察目标所组成的数组,数组项是由React.createRef构建出来的对象
  • callback: 当目标元素被曝光所需要触发的函数,该函数接受一个参数indexList,由被曝光元素在observerList数组中的索引组成
  • infinite:是否持续观察目标元素,默认值为false。(因为曝光打点一般只需上报一次)
  • opt:可以自定义曝光条件(值的构成参考MDN),默认为{ threshold: [1] },只有当目标元素完全暴露在可视区内才触发回调

UseIntersectionObserver 返回值:

  • 返回一个数组,数组的第一项元素由React的useState所返回。

2. 实现监听

定义 observeExposure 函数

// UseIntersectionObserver

const observeExposure = useCallback((list: ObserverList) => {}, [])
复制代码

使用useCallback减少不必要的重复函数声明

判断浏览器环境以及 list 的是否为空

   if (!IntersectionObserver) {
     throw new Error('Current browser does not support IntersectionObserver ')
   }
   if (list.length === 0) return
复制代码
  • 目标主流浏览器都已经支持该对象,但是还是兼容一些低版本浏览器
  • 当list为空列表是直接return

构造新的观察者实例

   
   // 当观察者存在时销毁该对象
   intersectionObserver && intersectionObserver.disconnect()
   // 构造新的观察者实例
   intersectionObserver = new IntersectionObserver(entries => {
     // 保存本次监听被曝光的元素
     let activeList: NumberList = []
     
     // 递归每一个本次被监听对象,如果按照曝光条件出现在可视区,则调用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)

         // 解除观察, 若需要无限观察则不取消监听
         !infinite &&
           intersectionObserver &&
           intersectionObserver.unobserve(list[index].current)
       }
     })
     
     // callback函数
     activeList.length > 0 && callback(activeList)
   }, opt)
   
复制代码

使用 intersectionObserver 监听 list 中的元素

    // 递归观察每一个元素
    list.forEach(item => {
      item.current &&
        intersectionObserver &&
        intersectionObserver.observe(item.current)
        
      // 可以兼容直接传入DOM节点。
      // if((>item).current) {
      //   intersectionObserver.observe((>item).current)
      // } else if ((item)) {
      //   intersectionObserver.observe((item))
      // }
    })
复制代码

3. 当每次被暴露的 setList 被调用时会使 list 被改变,此时需要重新监听

  useEffect(() => {
    observeExposure(list)
    
    // 当 umount 时取消链接
    return () => {
      intersectionObserver && intersectionObserver.disconnect()
    }
  }, [list])
复制代码

4. 完整代码实现

import { useState, useCallback, useEffect } from 'react'

type NumberList = number[]
type ObserverList = Arrayany>>
type CallbackFunction = (indexList: NumberList) => void
type ResultType = [React.Dispatchany>[]>>]

/**
 * UseIntersectionObserver
 * @param observerList 由被观察目标所组成的数组,数组项是由React.createRef构建出来的对象
 * @param callback 当目标元素被曝光所需要触发的函数,该函数接受一个参数indexList,由被曝光元素在observerList数组中的索引组成
 * @param infinite 是否持续观察目标元素,默认值为false。(因为曝光打点一般只需上报一次)
 * @param opt 可以自定义曝光条件(值的构成参考MDN),默认为{ threshold: [1] },只有当目标元素完全暴露在可视区内才触发回调
 */
function UseIntersectionObserver (
  observerList: ObserverList,
  callback: CallbackFunction,
  infinite: boolean = false,
  opt: IntersectionObserverInit = {}
): ResultType {
  // list 为需要监听的元素列表。setList做为UseIntersectionObserver函数的返回值,可以让调用者修改需要监听的 list 
  const [list, setList] = useState(observerList)

  // intersectionObserver: 观察者对象
  let intersectionObserver: IntersectionObserver | null = null

  const observeExposure = useCallback((list: ObserverList) => {
    if (typeof IntersectionObserver === 'undefined') {
      throw new Error('Current browser does not support IntersectionObserver ')
    }
    if (list.length === 0) return
    // 当观察者存在时销毁该对象
    intersectionObserver && intersectionObserver.disconnect()
    // 构造新的观察者实例
    intersectionObserver = new IntersectionObserver(entries => {
      // 保存本次监听被曝光的元素
      let activeList: NumberList = []

      // 递归每一个本次被监听对象,如果按照曝光条件出现在可视区,则调用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)

          // 解除观察, 若需要无限观察则不取消监听
          !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((>item).current) {
      //   intersectionObserver.observe((>item).current)
      // } else if ((item)) {
      //   intersectionObserver.observe((item))
      // }
    })
  }, [])

  useEffect(() => {
    observeExposure(list)

    // 当 umount 时取消链接
    return () => {
      intersectionObserver && intersectionObserver.disconnect()
    }
  }, [list])

  return [setList]
}

const useIntersectionObserver = UseIntersectionObserver

export default useIntersectionObserver

复制代码

使用案例

实现一个简单的商品列表曝光打点的案例

import Card from 'components/goods-card/goods-card'
import { connect } from 'react-redux'
import { getSinglePromotionList } from '../../page_components/promotion/redux/creator'
import React, { useEffect, useState, useCallback } from 'react'
import useIntersectionObserver from 'page_components/promotion/useIntersectionObserver'

const List = (props: { info: any; getData: any }) => {
  const { info, getData } = props
  
  // 被监听元素的列表
  const [refList, setRefList] = useState[]>([])

  const callback = useCallback((indexList: number[]) => {
    console.log(indexList)
  }, [])

  // 调用 
  const [setList] = useIntersectionObserver(refList, callback)

  // 当refList发生改变时,调用我们的Hook返回的方法以更新需要监听的元素
  useEffect(() => {
    setList(refList)
  }, [refList])

  // 当数据发生改变时,重新生成RefList
  useEffect(() => {
    const list: React.RefObject[] = info.list.map(() => React.createRef())
    setRefList(list)
  }, [info])

  // 发送请求,获取商品数据
  useEffect(() => {
    getData()
  }, [])

  return (
    
{info.list.map((item: any, index: number) => (
))}
) } const mapStateToProps = (state: any) => { return { info: state.promotionStore.singlePromotionInfo, userInfo: state.userInfo } } const mapDispatchToProps = (dispatch: any) => { return { getData: () => dispatch(getSinglePromotionList(params, silence)) } } export default connect( mapStateToProps, mapDispatchToProps )(List) 复制代码

案例效果

从动图可以看出,当card被曝光时打印出索引值,而已经被曝光的值不会再次曝光。



本文完~

如有疑问,欢迎指出~

你可能感兴趣的:(javascript)