手写React-Lazyload

155 阅读1分钟

前言

平时网页中会有很多图片,图片有一个加载过程,很多情况,我们是不需要一开始直接加载的,当浏览器的视图滚动到图片时,开始加载可以节省网络资源,这就是我们常说的懒加载,当然CSR下的组件配合React.lazy的动态导入也是可以进行懒加载的。

思路

使用IntersectionObserverAPI就可以实现懒加载的效果 我们可以用IntersectionObserver去监听需要懒加载的元素,当该元素滚动到视图区域时,请求该资源。

首先创建一个MyLazyLoad.tsx文件

import {
    CSSProperties,
    FC,
    ReactNode,
    useRef,
    useState
} from 'react';

interface MyLazyloadProps{
    className?: string,
    style?: CSSProperties,
    placeholder?: ReactNode,
    offset?: string | number,
    width?: number | string,
    height?: string | number,
    onContentVisible?: () => void,
    children: ReactNode,
}

const MyLazyload: FC<MyLazyloadProps> = (props) => {

    const {
        className = '',
        style,
        offset = 0,
        width,
        onContentVisible,
        placeholder,
        height,
        children
    } = props;

    const containerRef = useRef<HTMLDivElement>(null);
    const [visible, setVisible] = useState(false);

    const styles = { height, width, ...style };

    return <div ref={containerRef} className={className} style={styles}>
        {visible? children : placeholder}
    </div>
}

export default MyLazyload;

我们通过接收children元素来绑定lazy的效果,Props接收类名,样式,偏移量(当元素出现在视图的高度为多少像素时加载),宽度,高度,元素加载时的回调函数,占位元素,子元素(绑定lazy的元素)。

接下来我们new出 InterSectionObserve 用Ref去保存

const elementObserver = useRef<IntersectionObserver>();

useEffect(() => {
    const options = {
        rootMargin: typeof offset === 'number' ? `${offset}px` : offset || '0px',
        threshold: 0
    };

    elementObserver.current = new IntersectionObserver(lazyLoadHandler, options);

    const node = containerRef.current;

    if (node instanceof HTMLElement) {
        elementObserver.current.observe(node);
    }
    return () => {
        if (node && node instanceof HTMLElement) {
            elementObserver.current?.unobserve(node);
        }
    }
}, []);

这里的 rootMargin 就是距离多少进入可视区域就触发,和参数的 offset 一个含义。

threshold 是元素进入可视区域多少比例的时候触发,0 就是刚进入可视区域就触发。

然后用 IntersectionObserver 监听 div。

之后定义下 lazyloadHandler:

function lazyLoadHandler (entries: IntersectionObserverEntry[]) {
    const [entry] = entries;
    const { isIntersecting } = entry;

    if (isIntersecting) {
        setVisible(true);
        onContentVisible?.();

        const node = containerRef.current;
        if (node && node instanceof HTMLElement) {
            elementObserver.current?.unobserve(node);
        }
    }
};

当 isIntersecting 为 true 的时候,就是从不相交到相交,反之,是从相交到不相交。

这里设置 visible 为 true,回调 onContentVisible,然后去掉监听。

最终代码

import {
    CSSProperties,
    FC,
    ReactNode,
    useRef,
    useState
} from 'react';

interface MyLazyloadProps{
    className?: string,
    style?: CSSProperties,
    placeholder?: ReactNode,
    offset?: string | number,
    width?: number | string,
    height?: string | number,
    onContentVisible?: () => void,
    children: ReactNode,
}

const MyLazyload: FC<MyLazyloadProps> = (props) => {

    const {
        className = '',
        style,
        offset = 0,
        width,
        onContentVisible,
        placeholder,
        height,
        children
    } = props;

    const containerRef = useRef<HTMLDivElement>(null);
    const [visible, setVisible] = useState(false);

    const styles = { height, width, ...style };
    
    const elementObserver = useRef<IntersectionObserver>();
    
    function lazyLoadHandler (entries: IntersectionObserverEntry[]) {
        const [entry] = entries;
        const { isIntersecting } = entry;

        if (isIntersecting) {
            setVisible(true);
            onContentVisible?.();

            const node = containerRef.current;
            if (node && node instanceof HTMLElement) {
                elementObserver.current?.unobserve(node);
            }
        }
    };


useEffect(() => {
    const options = {
        rootMargin: typeof offset === 'number' ? `${offset}px` : offset || '0px',
        threshold: 0
    };

    elementObserver.current = new IntersectionObserver(lazyLoadHandler, options);

    const node = containerRef.current;

    if (node instanceof HTMLElement) {
        elementObserver.current.observe(node);
    }
    return () => {
        if (node && node instanceof HTMLElement) {
            elementObserver.current?.unobserve(node);
        }
    }
}, []);


    return <div ref={containerRef} className={className} style={styles}>
        {visible? children : placeholder}
    </div>
}

export default MyLazyload;