IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 虚拟滚动列表的 React 实现 -- 简单的等高虚拟列表 -> 正文阅读

[JavaScript知识库]虚拟滚动列表的 React 实现 -- 简单的等高虚拟列表

参考

虚拟滚动列表的大致方案

  • 虚拟列表的实现,大致上的思路就是:只加载可是区域范围内的列表项,发生滚动时,动态计算哪些列表项需要渲染
  • 为实现,我们需要(以下步骤以列表项高度 itemHeight 确定的情况为例,列表项高度不定时会麻烦一些)
    1. 有一个已知高度 height 的滚动容器 scrollContainer
    2. 计算所有数据 data 都渲染所需要占用的高度 itemHeight * data.length , 然后用这个高度撑开滚动容器,从而获得滚动条及滚动容器 scrollTop
    3. 计算当前可视区域所能渲染的列表条数 count = Math.ceil(height / itemHeight)
    4. 计算可视区域第一个列表项的索引 start
    5. 计算可视区域最后一个列表项的索引 end = start + count
    6. 渲染可视区域的数据 data.slice(start, end)
    7. 监听滚动容器的 scroll 事件,事件内获得当前滚动容器的 scrollTop, 并根据获得的 scrollTop 重新计算需要展示的第一个列表项索引 start = Math.floor(scrollTop / itemHeight)
    8. 计算当前滚动区域整体的偏移值 offset = scrollTop - (scrollTop % itemHeight) , 借助 css 属性 transform: translate3d(0, ${offset}px, 0) 使得可视区域平滑的上下移动(同时这个列表区域是 position: ‘absolute’ 脱离文档流的),每当 scrollTop 是 itemHeight 的整数倍时,设置偏移值的同时也会切换 start, 这就刚好可以给用户一个列表在正常滚动的错觉

0. 前期准备

  1. 准备一些可分页的数据,虚拟滚动列表,一般用来提升大数据量下的渲染性能,但是对于前端项目来说,数据是通过请求后端获得的,相比于大数据列表的渲染,大数据的请求更加耗时,所以,一般来说,实现虚拟滚动的同时,也要兼顾数据分页请求的考虑(一般是下拉到底后请求下一页数据)
  1. 准备一个可以动态获得容器宽高的控件,虚拟滚动列表需要获取高度来进行计算,一个“动态获得容器宽高的控件” 可以满足虚拟滚动列表在页面缩放或者其他影响展示高度的场景下拿到准确的高度值
/**
 * HOC 获得撑满所在区域的 宽高
 * NOTICE:
 * 1. 此组件的上层组件如果有 padding 处理,那么需要对传出的 width height 做些计算
 * 2. 此组件的下层组件不要设置 height=100% 或者 width=100% 否则可能会导致白屏,建议直接设置 style={width, height}
 * */

/**
 * ResizeObserver 可以用来监听 DOM 元素内容区域的边界改动
 * https://developer.mozilla.org/zh-CN/docs/Web/API/ResizeObserver
 * 但是有兼容问题,因此使用了第三方提供的 polyfill
 * 
*/
import ResizeObserverPolyfill from 'resize-observer-polyfill';

interface ISize {
  width: number;
  height: number;
}

interface IProps {
  children: (size: ISize) => React.ReactNode;
}

const AutoSizer: React.FC<IProps> = (props: IProps) => {
  const ref = React.useRef<HTMLDivElement>(null);
  const [size, setSize] = React.useState<ISize>({} as any);

  React.useEffect(() => {
    if (ref.current && ref.current.parentNode && ref.current.parentNode.ownerDocument) {
      const resizeObserver: ResizeObserver = new ResizeObserverPolyfill((entries) => {
        requestAnimationFrame(() => {
          if (!Array.isArray(entries) || !entries.length) {
            return;
          }
          if (ref && ref.current) {
            const target = entries[0].target as HTMLElement;
            const { offsetWidth, offsetHeight } = target;
            setSize({ width: offsetWidth, height: offsetHeight });
          }
        });
      });
      resizeObserver.observe(ref.current.parentNode as HTMLElement);
      return () => resizeObserver.disconnect();
    }
    return () => {};
  }, [ref]);

  const { children } = props;

  return (
    <div ref={ref} style={{ overflow: 'visible', width: 0, height: 0 }}>
      {children(size)}
    </div>
  );
};

export default React.memo(AutoSizer);

1. 普通的虚拟滚动列表 + 下拉动态加载

虚拟滚动组件封装

/*
 * 虚拟滚动列表
 * */
import VirtualLess from './Virtual.module.less';

interface IProps {
  /** 显示区域高度 */
  height: number;
  /** 显示区域宽度 */
  width?: number;
  /** 每一项 Item 的高度 */
  itemHeight: number;
  /** 总数据 */
  records: { [key: string]: any }[];
  /** 数据中的那一项作为唯一标识 */
  recordKeyName: string;
  /** Item 渲染方法 */
  renderItem: (record: IProps['records'][number]) => React.ReactNode;
  /** 显示到末尾时的回调 */
  onScrollToBottom: () => void;
  /** 为了显示效果富余显示的 Item 数量上下各加 number, 建议是一个比 分页请求数量小的值 */
  buffer?: number;
}

const VirtualList: React.FC<IProps> = (props: IProps) => {
  const scrollRef = React.useRef<HTMLDivElement>(null);
  const { height, width, itemHeight, renderItem, recordKeyName, records, buffer = 5,onScrollToBottom } = props;
  const [start, setStart] = React.useState(0);
  const [offset, setOffset] = React.useState(0);
  const visibleCount = Math.ceil(height / itemHeight);
  const end = start + visibleCount;
  const listNum = records.length;
  const listHeight = listNum * itemHeight;
  const displayRecords = records.slice(start, Math.min(end, listNum));

  const scrollListener = React.useCallback(() => {
    if (!scrollRef.current) return;
    const scrollTop = scrollRef.current.scrollTop;
    const nextStart = Math.floor(scrollTop / itemHeight);
    const nextOffset = scrollTop - (scrollTop % itemHeight);
    setStart(nextStart);
    setOffset(nextOffset);
    const nextEnd = nextStart + visibleCount + buffer;
    if (nextEnd >= listNum && onScrollToBottom) onScrollToBottom();
  }, [visibleCount, listNum, itemHeight]);

  React.useEffect(() => {
    const dom = scrollRef.current;
    scrollListener();
    if (dom) dom.addEventListener('scroll', scrollListener);
    return () => {
      if (dom) dom.removeEventListener('scroll', scrollListener);
    };
  }, [scrollListener]);

  return (
    <div
      className={VirtualLess.container}
      ref={scrollRef}
      style={{ height, width: width || '100%' }}
    >
      {/* pillar 负责撑开滚动列表的实际高度 最小值为 height + 1 视为了撑开一个滚动控件,防止一些边界情况导致的 onScrollToBottom 不触发 */}
      <div className={VirtualLess.pillar} style={{ height: Math.max(listHeight, height + 1) }} />
      {/* realList 通过定位展示在可视区域 */}
      <div className={VirtualLess.realList} style={{ transform: `translate3d(0, ${offset}px, 0)` }}>
        {displayRecords.map((record) => {
          return (
            <div className={VirtualLess.listItem} key={record[recordKeyName]}>
              {renderItem(record)}
            </div>
          );
        })}
      </div>
    </div>
  );
};

export default React.memo(VirtualList);

虚拟滚动组件的样式

.container {
  overflow-y: auto;
  position: relative;
}

.pillar{
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.realList {
  left: 0;
  right: 0;
  top: 0;
  position: absolute;
  text-align: center;
}
.listItem{
  width: 100%;
  border: 1px solid red;
}

虚拟滚动组件的使用 demo

import AutoSizer from './AutoSizer';
import VirtualList from './VirtualList';
import { getData } from './MockRequest'; // 一个获得列表数据的mock 接口

const Virtual = () => {
  const [data, setData] = React.useState<{ id: string; title: string }[]>([]);
  const [pageNumber, setPageNumber] = React.useState(1);
  const [isLoading, setIsLoading] = React.useState(true);

  React.useEffect(() => {
    setIsLoading(true);
    getData({ pageNumber, pageSize: 50 }).then((d) => {
      setIsLoading(false);
      setData(d);
    });
  }, []);

  const onScrollToBottom = React.useCallback(() => {
    if (isLoading) return; // 上一次请求结束才能进行下一页请求,避免频繁请求
    const nextPageNumber = pageNumber + 1;
    getData({ pageNumber: nextPageNumber, pageSize: 50 }).then((d) => {
      setPageNumber(nextPageNumber);
      setIsLoading(false);
      setData(data.concat(d));
    });
    setIsLoading(true);
  }, [data, isLoading, pageNumber]);

  const renderItem = React.useCallback((record: { id: string; title: string }) => {
    return <div style={{ height: 50 }}>{record.title}</div>;
  }, []);

  return (
    <div style={{ width: '100%', height: '100%' }}>
      <AutoSizer>
        {({ width, height = 0 }) => (
          <VirtualList
            width={width}
            height={height}
            itemHeight={50}
            records={data}
            recordKeyName="id"
            renderItem={renderItem}
            onScrollToBottom={onScrollToBottom}
          />
        )}
      </AutoSizer>
    </div>
  );
};

export default React.memo(Virtual);

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-04-22 18:27:19  更:2022-04-22 18:30:24 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 23:40:21-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码