• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

网页里可能会有很多图片,图片加载有一个过程,我们会在图片加载过程中展示占位图片。

并且我们不需要一开始就加载所有图片,而是希望在图片滚动到可视区域再加载。

这种效果我们会用 react-lazyload 来实现。

创建个项目:

npx create-vite

进入项目,安装 react-lazyload

npm install

npm install --save react-lazyload

npm install --save-dev @types/react-lazyload

npm install --save prop-types

prop-types 是 react-lazyload 用到的包。

去掉 index.css 和 StrictMode:

然后改下 App.tsx

import img1 from "./img1.png";
import img2 from "./img2.png";
import LazyLoad from "react-lazyload";

export default function App() {
    return (
        <div>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <p>xxxxxx</p>
            <LazyLoad placeholder={<div>loading...</div>}>
                <img src={img1} />
            </LazyLoad>
            <LazyLoad placeholder={<div>loading...</div>}>
                <img src={img2} />
            </LazyLoad>
        </div>
    );
}

在超出一屏的位置加载两张图片,用 LazyLoad 包裹。

可以看到,最开始展示 placeholder 的内容。

当图片划入可视区域后,会替换成图片:

在网络里也可以看到,当图片进入可视区域才会下载:

这就是 react-lazyload 的作用。

当然,它能做的可不只是懒加载图片,组件也可以。

我们知道,用 lazy 包裹的组件可以异步加载。

我们写一个 Guang.tsx

export default function Guang() {
    return "神说要有光";
}

然后在 App.tsx 里异步引入:

const LazyGuang = React.lazy(() => import("./Guang"));

import() 包裹的模块会单独打包,然后 React.lazy 是用到这个组件的时候才去加载。

试下效果:

可以看到,确实是异步下载了这个组件并渲染出来。

那如果我们想组件进入可视区域再加载呢?

这样:

react-lazyload 是进入可视区域才会把内容替换为 LazyGuang,而这时候才会去下载组件对应的代码。

效果就是这样的:

可以看到,Guang.tsx 的组件代码,img2.png 的图片,都是进入可视区域才加载的。

你还可以设置 offset,也就是不用到可视区域,如果 offset 设置 200,那就是距离 200px 到可视区域就触发加载:

可以看到,现在 img2 还没到可视区域就加载了。

知道了 react-lazyload 怎么用,那它是怎么实现的呢?

用前两节讲过的 IntersectionObserver 就可以实现。

我们来写一下:

src/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;

先看下 props:

className 和 style 是给外层 div 添加样式的。

placeholder 是占位的内容。

offset 是距离到可视区域多远就触发加载。

onContentVisible 是进入可视区域的回调。

然后用 useRef 保存外层 div 的引用。

用 useState 保存 visible 状态。

visible 的时候展示 children,否则展示 placeholder。

然后补充下 IntersectionObserver 监听 div 进入可视区域的情况:

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,然后去掉监听。

测试下:

可以看到,首先是图片加载,然后是组件加载,这说明 offset 生效了:

这样,我们就实现了 react-lazyload。

案例代码上传了小册仓库

总结

当图片进入可视区域才加载的时候,可以用 react-lazyload。

它支持设置 placeholder 占位内容,设置 offset 距离多少距离进入可视区域触发加载。

此外,它也可以用来实现组件进入可视区域时再加载,配合 React.lazy + import() 即可。

它的实现原理就是 IntersectionObserver,我们自己实现了一遍,设置 rootMargin 也就是 offset,设置 threshold 为 0 也就是一进入可视区域就触发。

图片、组件的懒加载(进入可视区域再触发加载)是非常常见的需求,不但要会用 react-lazyload 实现这种需求,也要能够自己实现。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
18.组件实战:Watermark防删除水印组件
Next
20.图解网页的各种距离