react-瀑布流虚拟滚动优化

关于虚拟滚动其实,react有一些现成的组件,例如react-window插件的VariableSizeGrid网格布局优化和VariableSizeList虚拟列表优化,

但是这些不符合我的业务需求,所以只能自己写一个瀑布流的虚拟滚动了

效果图如下:
在这里插入图片描述
在页面中只会展示在窗口中所看见的元素的结构,大大优化了渲染机制,适用于数据频繁变更,页面需要频繁的改变渲染的情况下,高性能优化
在这里插入图片描述
上下滚动也是很丝滑的,没有什么卡顿,几千条几万条数据不在话下
核心思想就是把渲染的结构,数据化,用数据思想存储好,把所有元素的位置计算好,这个元素需要渲染的时候可以直接定位到自己的位置
封装好的组件可以直接使用

/*
 * @Description: 虚拟滚动瀑布流组件
 * @Author: LuYuTing
 * @Date: 2024-03-06 15:12:03
 * @LastEditors: LuYuTing
 * @LastEditTime: 2024-03-08 15:22:59
 */
import { ReactNode, useEffect, useState } from 'react';
import { IWaterfallFlowDataType } from './data';
import { max } from 'lodash';

interface VirtualRollingWaterfallFlowProps {
  // 瀑布流总数据
  overallData: Array<any>;
  // 容器高度
  containerHeight: number;
  // 容器宽度
  containerwidth: number;
  // 每个元素的宽度
  itemWidth: number;
  // 元素最大宽度
  itemMaxWidth: number;
  // 元素的最大间隙,xxx以内
  maxSpace: number;
  // 监听父组件的滚动元素
  containerRef: React.RefObject<HTMLDivElement> | any;
  // 计算每个元素的高度
  handleItemHeight: (item: any, defaultWidth: number) => number;
  // 每个item的内容
  handleChildElement: (childData: any, childIndex: number) => ReactNode;
  // 是否默认保持在底部
  isDefaultBottom?: boolean;
  // 抛出的一些数据
  handleThrowData?: (manyData: any) => void;
}

const VirtualRollingWaterfallFlow: React.FC<VirtualRollingWaterfallFlowProps> = (props) => {
  const {
    overallData,
    containerHeight,
    containerwidth,
    itemWidth,
    itemMaxWidth,
    maxSpace,
    containerRef,
    handleItemHeight,
    handleChildElement,
    isDefaultBottom,
    handleThrowData,
  } = props;

  // 存储每一列当时的高度,判断下一个元素应该存放在哪里
  // const [heightArr, setHeightArr] = useState<number[]>([]);
  // 当前瀑布流的总数据,带元素高度和自身偏移量和子数据
  const [renderData, setRenderData] = useState<Array<IWaterfallFlowDataType>>([]);
  // 纵向数据存储,根据窗口截取数据
  const [verticalSortingData, setVerticalSortingData] = useState<Array<IWaterfallFlowDataType>>();
  // 可视区域数据,所渲染的数据
  const [visualAreaData, setVisualAreaData] = useState<Array<IWaterfallFlowDataType>>();

  // 找到长度最小的数组的下标
  const minArrIndex = (harr: number[]) => {
    let min = harr[0];
    let minIndex = 0;
    for (let i = 1; i < harr.length; i++) {
      if (harr[i] < min) {
        min = harr[i];
        minIndex = i;
      }
    }
    return minIndex;
  };

  /**
   * 组装每一个块块的左上两个方向的偏移量
   * @param height 元素高度
   * @param hArr 页面中各列元素高度数组
   * @param avegOffset 平均向左偏移量
   * @returns 返回当前元素的[左偏移量,顶偏移量]
   */
  const handleLeftandTopOffset = (
    height: number,
    hArr: number[],
    width: number,
    avegOffset: number,
  ) => {
    // 左left | 顶top
    const LeftandTop = [0, 0];
    const minIndex = minArrIndex(hArr);
    // 每个盒子的宽度包括间距
    LeftandTop[0] = minIndex * width + (minIndex + 1) * avegOffset;

    LeftandTop[1] = hArr[minIndex];
    hArr[minIndex] += height;

    return {
      minIndex,
      offset: {
        left: LeftandTop[0],
        top: LeftandTop[1],
      },
    };
  };
  // 根据给出的范围计算最合适的item宽度
  const handleSuitableWidth = (width: number) => {
    // 计算屏幕所能容纳的最大item个数
    const count = Math.floor(containerwidth / width);
    return [count, Math.floor((containerwidth % width) / (count + 1))];
  };

  // 初始化方法,
  const initializeMethod = () => {
    // 默认item宽度
    let defaultWidth = itemWidth;
    // 初始偏移量
    let averageOffset = handleSuitableWidth(defaultWidth)[1];
    // 判断间隙是否超过所给的参数
    while (averageOffset > maxSpace && defaultWidth < itemMaxWidth) {
      defaultWidth += 5;
      averageOffset = handleSuitableWidth(defaultWidth)[1];
    }

    const maxCount = handleSuitableWidth(defaultWidth);
    // setMaxcount(maxCount[0]);
    const initHeightArray = Array(maxCount[0]).fill(0);

    const newData: any = [];
    // 纵向排序数据
    const VSData: any = Array(maxCount[0])
      .fill(null)
      .map(() => []);

    overallData.forEach((value, index) => {
      const itemHeight = handleItemHeight(value, defaultWidth);
      const directionalOffset = handleLeftandTopOffset(
        itemHeight,
        initHeightArray,
        defaultWidth,
        averageOffset,
      );
      const item = {
        left: directionalOffset.offset.left,
        top: directionalOffset.offset.top,
        width: defaultWidth,
        height: itemHeight,
        child: value,
      };
      newData.push(item);
      VSData[directionalOffset.minIndex].push(item);
    });
    if (handleThrowData) {
      handleThrowData({ maxHeight: max(initHeightArray) });
    }
    if (containerRef.current && VSData.length) {
      let scrollTop = containerRef.current.scrollTop;
      if (isDefaultBottom) {
        // 找到现在最高的列
        scrollTop = max(initHeightArray) - containerHeight;
      }
      // 变化可渲染区域的数据
      getVisibleData(VSData, scrollTop);
    }
    setRenderData(newData);
    setVerticalSortingData(VSData);
    // setHeightArr(initHeightArray);
  };

  // 找出显示在屏幕区域的数据
  const getVisibleData = (data: any, scrollTop: number) => {
    const visibleData: any = [];
    // 遍历数据,判断每个元素是否出现在屏幕范围内
    data.forEach((column: IWaterfallFlowDataType[]) => {
      let accumulatedHeight = 0;
      column.forEach((item) => {
        // 确保当前元素在视窗范围内
        if (
          (accumulatedHeight + item.height > scrollTop &&
            accumulatedHeight < scrollTop + containerHeight) ||
          (accumulatedHeight < scrollTop + containerHeight &&
            accumulatedHeight + item.height > scrollTop)
        ) {
          visibleData.push(item);
        }
        accumulatedHeight += item.height;
      });
    });
    setVisualAreaData(visibleData);
  };

  // 滚动事件
  const handleScroll = () => {
    if (containerRef.current && verticalSortingData?.length) {
      const scrollTop = containerRef.current.scrollTop;
      // 变化可渲染区域的数据
      getVisibleData(verticalSortingData, scrollTop);
    }
  };

  useEffect(() => {
    initializeMethod();
  }, [overallData]);

  useEffect(() => {
    if (containerRef.current && renderData) {
      containerRef.current.addEventListener('scroll', handleScroll);
      return () => {
        // 在组件卸载时移除事件监听
        if (containerRef.current) {
          containerRef.current.removeEventListener('scroll', handleScroll);
        }
      };
    }
  }, [renderData]);

  useEffect(() => {
    if (visualAreaData && isDefaultBottom) {
      containerRef.current.scrollTop = containerRef.current.scrollHeight;
    }
  }, [visualAreaData, overallData]);

  return (
    <div
      style={{
        height: `${containerHeight}px`,
        width: `${containerwidth}px`,
      }}
    >
      {visualAreaData &&
        visualAreaData.map((item, index) => {
          return handleChildElement(item, index);
        })}
    </div>
  );
};

export default VirtualRollingWaterfallFlow;

使用方式,组件内都有参数注释的
在这里插入图片描述

  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
React-virtualized是一个非常流行的React库,它可以帮助我们实现大数据量的虚拟滚动效果。下面是一个使用React-virtualized实现虚拟滚动的表格的示例代码: 首先,我们需要安装React-virtualized库: ``` npm install react-virtualized --save ``` 然后,我们需要引入Table和Column组件: ``` import { Table, Column } from 'react-virtualized'; ``` 接下来,我们可以定义一个数据源,例如: ``` const list = [ { name: '张三', age: '18', address: '北京市海淀区' }, { name: '李四', age: '20', address: '北京市朝阳区' }, { name: '王五', age: '22', address: '北京市西城区' }, // ... // 这里可以添加更多的数据 ]; ``` 然后,我们可以定义一个Table组件,指定它的rowCount和rowGetter属性: ``` <Table rowCount={list.length} rowGetter={({ index }) => list[index]} > ``` 接下来,我们可以添加一些Column组件,定义每一列的属性: ``` <Column label="姓名" dataKey="name" width={100} /> <Column label="年龄" dataKey="age" width={100} /> <Column label="地址" dataKey="address" width={200} /> ``` 最后,我们需要在Table组件中添加一些属性,以启用虚拟滚动: ``` <Table rowCount={list.length} rowGetter={({ index }) => list[index]} headerHeight={20} rowHeight={30} width={600} height={400} > ``` 在上面的代码中,我们设置了headerHeight和rowHeight属性来指定表头和每一行的高度,width和height属性用于指定表格的宽度和高度。React-virtualized会自动根据这些属性来计算出需要渲染的行数,并且只渲染当前可见的行,以实现虚拟滚动的效果。 完整的代码示例: ``` import React, { Component } from 'react'; import { Table, Column } from 'react-virtualized'; const list = [ { name: '张三', age: '18', address: '北京市海淀区' }, { name: '李四', age: '20', address: '北京市朝阳区' }, { name: '王五', age: '22', address: '北京市西城区' }, // ... // 这里可以添加更多的数据 ]; class VirtualTable extends Component { render() { return ( <Table rowCount={list.length} rowGetter={({ index }) => list[index]} headerHeight={20} rowHeight={30} width={600} height={400} > <Column label="姓名" dataKey="name" width={100} /> <Column label="年龄" dataKey="age" width={100} /> <Column label="地址" dataKey="address" width={200} /> </Table> ); } } export default VirtualTable; ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值