React Hooks自定义一个图片懒加载hooks

·  阅读 3764

图片懒加载也是一个老生常谈的话题了,假如一个网站的图片很多,如果不对图片做懒加载处理,那么一打开这个页面,所有的图片都会向服务器发起资源请求,势必会出现资源浪费的情况(如果用户打开了这个网站,并没有往下面下方浏览,只是停留在了当前的这个位置,然后退出了网页,那么那些不需要展示的图片也已经加载完成了,这就造成了资源浪费);对于一些个人网站来说,图片懒加载更是必要的。

图片懒加载的定义

展示那些需要展示给用户的图片,话句话说,就是只有用户浏览到的地方,才需要加载出图片。

图片懒加载的实现

1.不直接给img标签的src属性赋值,可以先给其data-src属性值赋值图片的地址
2.当图片到顶部的距离大于可视区域和滚动区域之和的时候,再将data-src的值赋值给src

普通实现,使用getBoundingClientRect

getBoundingClientRect 返回元素的大小及其相对于视口的位置,是一个集合;集合中有top, right, bottom, left等属性。

top:元素上边到视窗上边的距离;
right:元素右边到视窗左边的距离;
bottom:元素下边到视窗上边的距离;
left:元素左边到视窗左边的距离;

先实现一个throttle节流的自定义hooks,scroll事件一般都要配合其使用


import { useRef, useEffect } from 'react'

const useThrottle = (fn, wait, deps = []) => {
    const prev = useRef(0)

    useEffect(() => {
        const curr = Date.now()
        if (curr - prev.current > wait) {
            fn()
            prev.current = curr
        }
    }, deps)
}

export default useThrottle
复制代码

实现图片懒加载hooks useImageLazy

首先我们下载一个强大的react-hooks自定义库ahooks

yarn add ahooks
复制代码
import { useState, useEffect, useRef } from 'react';
import { useMemoizedFn, useMount, useUnmount } from 'ahooks';
import useThrottle from './useThrottle';

const useImageLazy = (domList) => {
    const [scrollCount, setScrollCount] = useState(0);
    const scrollEvent = useRef();

    const getTop = () => {
        // 元素滚动的距离
        let scrollTop =
            document.documentElement.scrollTop || document.body.scrollTop; 
            
        // 当前视窗的可视区域    
        let clientHeight =
            document.documentElement.clientHeight || document.body.clientHeight; 
            
        let len = domList.length;

        for (let i = 0; i < len; i++) {
            // 元素距离页面顶部的距离
            let { top } = domList[i].getBoundingClientRect(); 
            
            // 当图片到顶部的距离大于可视区域和滚动区域之和的时候,再将data-src的值赋值给src  
            if (top < scrollTop + clientHeight) { 
                
                // 当元素已经拥有src属性时,不再为其进行src属性赋值操作
                if (!domList[i].src) { 
                    domList[i].src = domList[i].dataset.src;
                }
            }
        }
    };
    
    // 每次滚动我们需要改变scrollCount,用来更新useThrottle来重新计算时间差
    scrollEvent.current = useMemoizedFn(() => {
        setScrollCount(scrollCount + 1);
    });
    
    useMount(() => {
        document.addEventListener('scroll', scrollEvent.current);
    })
    
    useUnmount(() => {
        document.removeEventListener('scroll', scrollEvent.current);
    })
    
    
    useThrottle(
        () => {
            getTop();
        },
        200, // 这里我们的节流传入的时间间隔为200ms
        [scrollCount],
    );
};

export default useImageLazy;
复制代码

在组件里使用:

import React, { useRef } from 'react';
import useImageLazy from './useImageLazy';

const imgList = Array.from(
  { length: 30 },
  (item, index) =>
    (item = `https://www.maojiemao.com/public/svg/gen${index + 5}.png`),
);

const useImageLazyDemo = () => {
  const domRef = useRef([]);
  useImagelazy(domRef.current);

  return (
    <>
      {imgList.map((item, index) => (
        <img
          ref={(el) => (domRef.current[index] = el)}
          key={`lazy-${index}`}
          data-src={item}
          style={{
            display: 'block',
            width: '30px',
            height: '30px',
            marginTop: '50px',
          }}
        />
      ))}
    </>
  );
};

export default useImageLazyDemo;

复制代码

使用IntersectionObserver API 自定义hooks

主要使用的是 IntersectionObserver 这个Web API,没有了解过的可以移步链接稍微看一看,上代码:

import { useRef } from 'react'
import { useDeepCompareEffect, useMount, useUnmount } from 'ahooks'

const useIntersectionObserver = (domList, deps = [0]) => {
    // 接收两个参数,dom元素的class和指定交叉比例(threshold)的依赖项

    const ioRef = useRef()

    useMount(() => {
        ioRef.current = new IntersectionObserver((entries) => { // 观察者
            entries.forEach((item) => { // entries 是被监听的dom集合,是一个数组
                if (item.intersectionRatio <= 0) return // intersectionRatio 是可见度 如果当前元素不可见就结束该函数。
                const { target } = item
                target.src = target.dataset.src // 将 h5 自定义属性赋值给 src (进入可见区则加载图片)
            })
        }, {
            threshold: deps // 用来指定交叉比例,决定什么时候触发回调函数,是一个数组,默认是[0]。
        });
    })

    useUnmount(() => {
        ioRef.current.disconnect(); // 组件卸载时关闭观察器
    })
    
    // useDeepCompare用法同useEffect一样,不同的是依赖项里如果有复杂数据,如对象、数组等,对比的是每一项是否相等,这样在对象或者数组没有变化时,就不会再次执行回调函数。
    useDeepCompareEffect(() => {
        domList.forEach(item => ioRef.current.observe(item)); // observe 开始观察,接受一个DOM节点对象
    }, [domList])

}

export default useIntersectionObserver

复制代码

在组件里使用:

import React, { useRef } from 'react';
import useIntersectionObserver from './useIntersectionObserver';
import { useRequest, useSafeState } from 'ahooks';
   
// 模拟从接口获取数据
const getDomService = () => {
  return new Promise((resovle) => {
    const imgList = Array.from(
      { length: 30 },
      (item, index) =>
        (item = `https://www.maojiemao.com/public/svg/gen${index + 5}.png`),
    );
    setTimeout(() => {
      resovle({
        message: 'SUCCESS',
        code: 200,
        data: imgList,
      });
    }, 3000);
  });
};

const Test = () => {
  const domRef = useRef([]); // 获取到的dom集合
  const [domList, setDomList] = useSafeState([]); // 同样是dom集合,将domRef收集起来更新组件状态,触发useIntersectionObserver执行

  const { data = [] } = useRequest(getDomService);

  useIntersectionObserver(domList, [1]);

  return (
    <>
      {data?.data?.map((item, index) => (
        <img
          key={`lazy-${index}`}
          className="avatar-img"
          data-src={item}
          style={{
            display: 'block',
            width: '50px',
            height: '50px',
            marginBttom: '20px',
          }}
          ref={(el) => {
            // 异步渲染得到的元素,先通过domRef.current收集起来
            if (el) {
              domRef.current[index] = el;
            }
            
            // 当domRef.current将所有的img元素收集起来之后,更新domList的状态,触发组件更新-> useIntersectionObserver再次执行
            if (domRef.current?.length === data?.data?.length) {
              setDomList(domRef.current);
            }
          }}
        />
      ))}
    </>
  );
};

export default Test;
复制代码
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改