IntersectionObserver及自定义Hooks

1,657 阅读3分钟

IntersectionObserver

作用: 观察一个元素是否在视口可见

用途:无限滚动、图片懒加载、控制动画/视频执行(性能优化)

使用场景:判断某个元素是否进入了视口,传统的实现方法是,监听scroll事件,调用目标元素的getBoundingClientRect()方法,得到对于视口左上角的位置,再去判断是否在视口之内,缺点是由于scroll事件频发,计算量很大,容易造成性能问题

IntersectionObserver浏览器提供的原生方法,可以自动观察元素是否可见

cloud.tencent.com/developer/a…

图片懒加载

原理:

  • 图片是否进入可视区域 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
   }
 }, []);

列表下拉加载

原理: loading是否进入可视区域: 如果进入可视区域 则加载下一页

实际上除了接口错误,和请求回来的数据长度小于设定的pageSize外,loading一直存在,只不过被请求回来的数据挤下去了

html结构:

<div className="resume-list-wrapper">
      {renderList()}
      {
        hasMore ? (
          <div className="loader" ref={loaderRef} style={{ height: hasLoadOneTime ? '100%' : 'calc(100vh - 100px)' }}>
            <Icon type="loading" />数据加载中...
          </div>
        ) : (
          <div className="no-more-data">{resumeDataList.length ? '没有更多数据了~' : null}</div>
        )
      }
      {ScrollTopIcon}
</div>
  const loadMore = useCallback((pageNumber) => {
    if (hasMore) {
      run({ pageNo: pageNumber, pageSize: 50 }).then((res) => {
        setHasLoadOneTime(true);
        if (res && res.list) {
          if (res.list.length < 50) setHasMore(false);
          setResumeDataList((preList) => [...preList, ...res.list]);
        }
      });
    }
  }, [run, hasMore]);

  useInfiniteScroll({ loaderRef, loadMore, pageNo: 1 });

自定义 useInfiniteScroll Hooks

import { useEffect, useRef } from 'react';

// 请务必确保首次加载能将首屏占满,并且每次加载数据后能将屏幕铺满,loader展示在隐藏处
// 用户需自己根据接口等状态,维护loaderRef的展示,内部会根据loaderRef判断是否需要去执行loadMore函数
interface ITypes {
  loaderRef: { current: undefined | HTMLElement };
  loadMore: (pageNo: number) => void;
  pageNo: number;
}
export default function useInfiniteScroll({ loaderRef, loadMore, pageNo }: ITypes) {
  // 这里使用pageNoRef的原因是缓存ref,这样不会频繁调用一下的useeffect函数,就不会重复创建IntersectionObserver了
  const pageNoRef = useRef(pageNo);

  useEffect(() => {
    let ob: IntersectionObserver;
    // 只有当loaderRef.current存在时,才有必要创建observer。如果loader不存在了,就没必要ob了
    // 外部组件需要自己设置hasMore变量,自行控制loader组件的展示
    if (loaderRef.current) {
      ob = new IntersectionObserver((entries) => {
        const entry = entries[0];
        // isIntersecting 表示目标元素loaderRef 是否在可视区,在可视区 再去加载
        if (entry.isIntersecting) {
          loadMore(pageNoRef.current);
          pageNoRef.current += 1;
        }
      });
      ob.observe(loaderRef.current); // 监听元素
    }
    return () => {
      ob && ob.disconnect(); // 组件卸载时 取消监听
    };
  }, [loadMore, loaderRef]);
}

自定义Hooks

在项目中使用自定义useScrollTop

当页面滚动的距离大于屏幕的高度时,出现圆形按钮,点击可滚动到页面最上方

import React, { useState, useEffect } from 'react';
import { Icon } from '@ss/mtd-react';
import debounce from 'lodash/debounce';

const useScrollTop = ({ className = 'float-icon' }) => {
  const [iconVisible, setIconVisible] = useState(false);

  useEffect(() => {
    const callback = debounce((e) => {
      // clientHeight 屏幕高度 scrollTop 距离顶部距离 scrollHeight 滚动视图整体高度
      const { clientHeight = 0, scrollTop = 0 } = e?.target?.scrollingElement;
      if (scrollTop > clientHeight) { setIconVisible(true); } else {
        setIconVisible(false);
      }
    }, 200);
    document.addEventListener('scroll', callback);
    return () => {
      document.removeEventListener('scroll', callback);
    };
  }, []);

  if (iconVisible) {
    return (
      <div
        className={className}
        onClick={() => {
          window.scrollTo({
            top: 0,
            behavior: 'smooth',
          });
        }}
      ><Icon type="top" />
      </div>
    );
  }
  return null;
};

export default useScrollTop;

const ScrollTopIcon = useScrollTop({});