如何实现一个可以加载10万+数据的长列表(二)

如何实现一个可以加载10万+数据的长列表(二)

如何实现非固定高度的长列表

在之前的基础上需要做一下改动

在这里插入图片描述
设置两个容器,一个为真实的滚动区容器,另一个为显示容器。
显示容器固定显示,通过更改滚动容器高度来适配

结构如下:

     <div ref={this.domRef} className={`virtual-list-scroll ${className}`} style={style} onScroll={this.handleScroll}>
        <div className="virtual-list-grid-virtual" style={{ height: this.manage.getHeight() }} />
        <div ref={this.containerRef} className="virtual-list-grid">
          {dataSource.map((item, index) => {
            if ((index >= start && index <= end) || debug) {
              return (
                <Cell
                  key={index - start}
                  index={index}
                  max={dataSource.length - 1}
                  manage={manage}
                  cellRender={cellRender}
                  item={item}
                />
              );
            }
            return null;
          })}
        </div>
      </div>

如此,在滚动时更新起始索引和结束索引达到数据更新效果。

当某个节点加载后在更新对应节点的缓存信息,

 // 更新缓存数据
 measure(rowIndex: number, dom: HTMLElement): void {
    this.checkIndex(rowIndex);

    const top = this.topList[rowIndex];
    if (!top) {
      return;
    }

    const height = dom.offsetHeight;
    const offset = height - top.height;
    this.topList[rowIndex] = { top: top.top, height };
    if (offset) {
      for (let i = rowIndex + 1; i < this.topList.length; i++) {
        this.topList[i].top += offset;
      }
    }
  }

如此便达到了自适应的高度。

全部代码贴上:

滚动区

import * as React from 'react';

import Grid from './Grid';
import { IListOption } from '../../type';
import { PositionManage, IPositionManage } from './Measure';

import './ScrollBar.css';

interface IProps<T> {
  overScan?: number; // 超出的预加载数量
  style?: React.CSSProperties; // 滚动的样式
  debug?: boolean;
  className?: string;
  estimateMinHeight?: number;
  dataSource: T[]; // 数据
  cellRender: (node: T, option: IListOption) => React.ReactNode; // 自定义节点渲染
}

interface IStates<T> {
  scrollTop: number;
}

/**
 * 虚拟滚动列表
 *
 * 仅显示可见区域的节点
 * TODO: 实现行高自适配
 */
class ScrollBar<T> extends React.Component<React.PropsWithChildren<IProps<T>>, IStates<T>> {
  private domRef: React.RefObject<HTMLDivElement> = React.createRef();
  private gridRef: React.RefObject<Grid<T>> = React.createRef();
  private manage: IPositionManage;
  private startIndex = 0;
  private endIndex = 0;
  private originStartIdx = 0;
  private limit = 0;
  private total = 0;
  private estimateMinHeight = 25; // 预估节点高度
  private contentHeight = 0;

  static defaultProps = {
    overScan: 2,
  };

  constructor(props: IProps<T>) {
    super(props);
    this.total = props.dataSource.length;
    this.estimateMinHeight = props.estimateMinHeight ?? this.estimateMinHeight;
    this.manage = new PositionManage(this.total - 1);
    this._initCachedPositions();
    this.state = {
      scrollTop: 0,
    };
  }

  componentDidMount(): void {
    this.contentHeight = this.domRef.current?.getBoundingClientRect().height;
    this.limit = Math.ceil(this.contentHeight / this.estimateMinHeight);
    this.startIndex = Math.max(this.originStartIdx - this.props.overScan, 0);
    this.endIndex = Math.min(this.originStartIdx + this.limit + this.props.overScan, this.total - 1);
    this.setState({ scrollTop: this.domRef.current.scrollTop });
  }

  private _initCachedPositions = (): void => {
    const { estimateMinHeight } = this;
    for (let i = 0; i < this.total; ++i) {
      this.manage.cache(i, {
        top: i * estimateMinHeight,
        height: estimateMinHeight,
      });
    }
  };

  private getStartIndex = (scrollTop = 0): number => {
    return this.manage.getStartIndex(scrollTop);
  };

  private handleScroll = (): void => {
    const {
      total,
      originStartIdx,
      props: { overScan },
    } = this;
    const scrollTop = this.domRef.current.scrollTop;

    const currIndex = this.getStartIndex(scrollTop) - this.props.overScan;

    if (originStartIdx !== currIndex) {
      this.originStartIdx = currIndex;
      this.startIndex = Math.max(currIndex - overScan, 0);
      const endIndex = this.getStartIndex(scrollTop + this.contentHeight);
      this.endIndex = Math.min(endIndex + overScan, total - 1);
      const prev = this.manage.get(this.startIndex - 1);
      this.gridRef.current.containerRef.current.style.top = `${prev ? prev?.top + prev?.height : 0}px`;
      this.setState({
        scrollTop: scrollTop,
      });
    }
  };

  scrollToRow(index: number): void {
    this.domRef.current.scrollTo({ top: this.manage.get(index).top });
    window.setTimeout(() => this.domRef.current.scrollTo({ top: this.manage.get(index).top }));
  }

  render(): React.ReactNode {
    const {
      startIndex,
      endIndex,
      props: { className, style, dataSource, debug, cellRender },
    } = this;

    return (
      <div ref={this.domRef} className={`virtual-list-scroll ${className}`} style={style} onScroll={this.handleScroll}>
        <div className="virtual-list-grid-virtual" style={{ height: this.manage.getHeight() }} />
        <Grid
          ref={this.gridRef}
          start={startIndex}
          debug={debug}
          manage={this.manage}
          end={endIndex}
          dataSource={dataSource}
          cellRender={cellRender}
        />
      </div>
    );
  }
}

export default ScrollBar;

网格

import * as React from 'react';

import { IListOption } from '../../type';
import { IPositionManage } from './Measure';

import Cell from './Cell';

import './Grid.css';

interface IProps<T> {
  manage: IPositionManage;
  dataSource: T[];
  debug?: boolean;
  start: number;
  end: number;
  cellRender: (node: T, option: IListOption) => React.ReactNode;
}

/**
 * 网格类
 * TODO: 实现多行多列布局
 */
class Grid<T> extends React.Component<IProps<T>> {
  public containerRef: React.RefObject<HTMLDivElement> = React.createRef();

  render(): React.ReactNode {
    const { start, debug, manage, dataSource, end, cellRender } = this.props;

    return (
      <div ref={this.containerRef} className="virtual-list-grid">
        {dataSource.map((item, index) => {
          if ((index >= start && index <= end) || debug) {
            return (
              <Cell
                key={index - start}
                index={index}
                max={dataSource.length - 1}
                manage={manage}
                cellRender={cellRender}
                item={item}
              />
            );
          }
          return null;
        })}
      </div>
    );
  }
}

export default Grid;

import * as React from 'react';

import { IListOption } from '../../type';
import { IPositionManage } from './Measure';

import './Cell.css';

interface IProps<T> {
  manage: IPositionManage;
  index: number;
  max: number;
  item: T;
  cellRender: (node: T, option: IListOption) => React.ReactNode;
}
interface IStates {}

/**
 * 当行类
 * 自定义的渲染
 */
class Cell<T> extends React.Component<IProps<T>, IStates> {
  public ref: React.RefObject<HTMLDivElement> = React.createRef();

  private measured: boolean = false;

  componentDidMount(): void {
    this._measure();
  }

  componentDidUpdate(): void {
    this._measure();
  }

  private _measureOut = (index: number) => {
    this.props.manage.measure(index, this.ref.current);
  };

  private _measure = () => {
    if (this.ref.current && !this.measured) {
      this.measured = true;
      this._measureOut(this.props.index);
    }
  };

  render(): React.ReactNode {
    const { item, index, cellRender } = this.props;
    return (
      <div ref={this.ref} className="virtual-list-cell">
        {cellRender(item, { index, measure: this._measureOut })}
      </div>
    );
  }
}

export default Cell;

测量类

import { binarySearch } from '../../helper';
import { CompareResult } from '../../type';

interface IPositionData {
  top: number;
  height: number;
}

interface IPositionManage {
  cache(rowIndex: number, top: IPositionData): void;
  get(rowIndex: number): IPositionData | undefined;
  measure(index: number, dom: HTMLElement): void;
  getHeight(): number;
  getStartIndex(top: number): number;
}

class PositionManage {
  private topList: IPositionData[] = [];

  constructor(public max: number) {}

  checkIndex(rowIndex: number): void {
    if (rowIndex > this.max || rowIndex < 0) {
      throw new Error('错误索引!');
    }
  }

  cache(rowIndex: number, position: IPositionData): void {
    this.checkIndex(rowIndex);
    this.topList[rowIndex] = position;
  }

  getHeight(): number {
    return this.topList[this.max].top + this.topList[this.max].height;
  }

  measure(rowIndex: number, dom: HTMLElement): void {
    this.checkIndex(rowIndex);

    const top = this.topList[rowIndex];
    if (!top) {
      return;
    }

    const height = dom.offsetHeight;
    const offset = height - top.height;
    this.topList[rowIndex] = { top: top.top, height };
    if (offset) {
      for (let i = rowIndex + 1; i < this.topList.length; i++) {
        this.topList[i].top += offset;
      }
    }
  }

  get(rowIndex: number): IPositionData | undefined {
    return this.topList[rowIndex];
  }

  getStartIndex(top: number): number {
    return binarySearch<IPositionData, number>(
      this.topList,
      top,
      (currentValue: IPositionData, targetValue: number) => {
        const currentCompareValue = currentValue.top;

        if (currentCompareValue === targetValue) {
          return CompareResult.eq;
        }

        if (currentCompareValue < targetValue) {
          return CompareResult.lt;
        }

        return CompareResult.gt;
      },
    );
  }
}

export { PositionManage, IPositionManage };

使用方式:

 <VirtualList
    ref={this.scrollRef}
    style={{ width: 150, height: 500 }}
    className={`custom-virtual-list ${theme}`}
    dataSource={dataSource}
    cellRender={this.cellRender}
  />
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值