关于list:大数据列表渲染系列二极简实现

61次阅读

共计 5687 个字符,预计需要花费 15 分钟才能阅读完成。

本节,咱们实现一个极简版的虚构列表,固定尺寸的虚构列表,麻雀虽小,却是五脏俱全哦!

需要

实现一个固定尺寸的虚构渲染列表组件,props 属性如下:

props: { 
    width: number; 
    height: number;
    itemCount: number;
    itemSize: number;
}

应用形式:

const Row = (..args) => (<div className="Row"></div>);
<List className={"List"} width={300} height={300} itemCount={10000} itemSize={40}>
    {Row}
</List>

实现

什么技术栈都能够,这里我的项目应用的 react,那就选用 react 来实现。

初始化我的项目

应用 create-react-app 初始化一个利用,而后启动,清理掉 demo 的代码。

虚构列表

依据上一节的剖析,咱们核心技术实现是 一个 render 渲染函数,用来渲染数据;一个 onScroll 函数监听滚动事件,去更新 数据区间 [startIndex, endIndex],而后从新 render。大略伪代码如下:

class List extends React.PureComponent {state = {};
  render() {};
  onScroll() {};
}

接下来咱们进行细节填充实现,首先咱们须要依据数据渲染出第一屏初始化的 dom,即要先实现 render 函数逻辑,咱们采纳相对定位的形式进行 dom 排版。

render() {
    // 从 props 解析属性
    const {
      children,
      width,
      height,
      itemCount,
      layout,
      itemKey = defaultItemKey,
    } = this.props;
    // 预留方向设定属性
    const isHorizontal = layout === "horizontal";
    // 假如有一个函数_getRangeToRender 能够帮咱们计算出 渲染区间
    const [startIndex, stopIndex] = this._getRangeToRender();
    const items = [];
    if (itemCount > 0) {
      // 循环创立元素
      for (let index = startIndex; index <= stopIndex; index++) {
        items.push(
          createElement(children, {data: {},
            key: itemKey(index),
            index,
            style: this._getItemStyle(index), // 帮忙计算 dom 的地位款式
          })
        );
      }
    }
    // 假如 getEstimatedTotalSize 函数能够帮忙咱们计算出总尺寸
    const estimatedTotalSize = getEstimatedTotalSize(this.props,);
    return createElement(
      "div",
      {
        onScroll: this.onScroll,
        style: {
          position: "relative",
          height,
          width,
          overflow: "auto",
          WebkitOverflowScrolling: "touch",
          willChange: "transform",
        },
      },
      createElement("div", {
        children: items,
        style: {
          height: isHorizontal ? "100%" : estimatedTotalSize,
          pointerEvents: "none",
          width: isHorizontal ? estimatedTotalSize : "100%",
        },
      })
    );
}

OK,到了这里 render 函数的逻辑就写完了,是不是超级简略。接下来咱们实现以下 render 函数外面应用到的辅助函数.

getEstimatedTotalSize

先看 getEstimatedTotalSize 计算总尺寸函数的实现:

// 总尺寸 = 总个数 * 每个 size 
export const getEstimatedTotalSize = ({itemCount, itemSize}) =>
  itemSize * itemCount;

_getRangeToRender

计算须要渲染的数据区间函数实现

  _getRangeToRender() {
    // overscanCount 是缓冲区的数量,默认设置 1
    const {itemCount, overscanCount = 1} = this.props;
    // 曾经滚动的间隔,初始默认 0
    const {scrollOffset} = this.state;

    if (itemCount === 0) {return [0, 0, 0, 0];
    }
    // 辅助函数,依据 滚动间隔计算出 区间开始的索引
    const startIndex = getStartIndexForOffset(
      this.props,
      scrollOffset,
    );
    // 辅助函数,依据 区间开始的索引计算出 区间完结的索引
    const stopIndex = getStopIndexForStartIndex(
      this.props,
      startIndex,
      scrollOffset,
    );
    return [Math.max(0, startIndex - overscanCount),
      Math.max(0, Math.min(itemCount - 1, stopIndex + overscanCount)),
      startIndex,
      stopIndex,
    ];
  }
}


// 计算区间开始索引,滚动间隔 除以 每个单元尺寸 就是  startIndex
export const getStartIndexForOffset = ({itemCount, itemSize}, offset) =>
  Math.max(0, Math.min(itemCount - 1, Math.floor(offset / itemSize)));
// 计算区间完结索引,开始索引 + 可见区域 size / itemSize 即可
export const getStopIndexForStartIndex = ({ height, itemCount, itemSize, layout, width},
  startIndex,
  scrollOffset
) => {
  const isHorizontal = layout === "horizontal";
  const offset = startIndex * itemSize;
  const size = isHorizontal ? width : height;
  const numVisibleItems = Math.ceil((size + scrollOffset - offset) / itemSize);
  return Math.max(
    0,
    Math.min(
      itemCount - 1,
      startIndex + numVisibleItems - 1
    )
  );
};

计算元素地位 _getItemStyle

计算形式:依据 index * itemSize 即可计算出 position

  _getItemStyle = (index) => {const { layout} = this.props;

    let style;

    const offset = index * itemSize;
    const size = itemSize;

    const isHorizontal = layout === "horizontal";
    
    const offsetHorizontal = isHorizontal ? offset : 0;
    style = {
      position: "absolute",
      left: offsetHorizontal,
      top: !isHorizontal ? offset : 0,
      height: !isHorizontal ? size : "100%",
      width: isHorizontal ? size : "100%",
    };

    return style;
  };

好了,到此地位,render 函数的所有逻辑全副实现结束了。

监听滚动 onScroll 实现

最初一步,只须要监听 onScroll 事件,更新 数据索引区间,咱们的性能就欠缺了

  // 非常简单,只是一个 setState 操作,更新滚动间隔即可
 _onScrollVertical = (event) => {const { clientHeight, scrollHeight, scrollTop} = event.currentTarget;
    this.setState((prevState) => {if (prevState.scrollOffset === scrollTop) {return null;}
      const scrollOffset = Math.max(
        0,
        Math.min(scrollTop, scrollHeight - clientHeight)
      );
      return {scrollOffset,};
    });
  };

残缺代码


class List extends PureComponent {
  _outerRef;

  static defaultProps = {
    layout: "vertical",
    overscanCount: 2,
  };

  state = {
    instance: this,
    scrollDirection: "forward",
    scrollOffset: 0,
  };
  render() {
    const {
      children,
      width,
      height,
      itemCount,
      layout,
      itemKey = defaultItemKey,
    } = this.props;

    const isHorizontal = layout === "horizontal";

    // 监听滚动函数
    const onScroll = isHorizontal
      ? this._onScrollHorizontal
      : this._onScrollVertical;

    const [startIndex, stopIndex] = this._getRangeToRender();
    const items = [];
    if (itemCount > 0) {for (let index = startIndex; index <= stopIndex; index++) {
        items.push(
          createElement(children, {data: {},
            key: itemKey(index),
            index,
            style: this._getItemStyle(index),
          })
        );
      }
    }
    const estimatedTotalSize = getEstimatedTotalSize(this.props);
    return createElement(
      "div",
      {
        onScroll,
        style: {
          position: "relative",
          height,
          width,
          overflow: "auto",
          WebkitOverflowScrolling: "touch",
          willChange: "transform",
        },
      },
      createElement("div", {
        children: items,
        style: {
          height: isHorizontal ? "100%" : estimatedTotalSize,
          pointerEvents: "none",
          width: isHorizontal ? estimatedTotalSize : "100%",
        },
      })
    );
  }
  _onScrollHorizontal = (event) => {};
  _onScrollVertical = (event) => {const { clientHeight, scrollHeight, scrollTop} = event.currentTarget;
    this.setState((prevState) => {if (prevState.scrollOffset === scrollTop) {return null;}
      const scrollOffset = Math.max(
        0,
        Math.min(scrollTop, scrollHeight - clientHeight)
      );
      return {scrollOffset,};
    });
  };

  _getItemStyle = (index) => {const { layout} = this.props;

    let style;

    const offset = getItemOffset(this.props, index, this._instanceProps);
    const size = getItemSize(this.props, index, this._instanceProps);

    const isHorizontal = layout === "horizontal";

    const offsetHorizontal = isHorizontal ? offset : 0;
    style = {
      position: "absolute",
      left: offsetHorizontal,
      top: !isHorizontal ? offset : 0,
      height: !isHorizontal ? size : "100%",
      width: isHorizontal ? size : "100%",
    };

    return style;
  };

  // 计算出须要渲染的数据索引区间
  _getRangeToRender() {const { itemCount, overscanCount = 1} = this.props;
    const {scrollOffset} = this.state;

    if (itemCount === 0) {return [0, 0, 0, 0];
    }

    const startIndex = getStartIndexForOffset(
      this.props,
      scrollOffset
    );
    const stopIndex = getStopIndexForStartIndex(
      this.props,
      startIndex,
      scrollOffset
    );

    return [Math.max(0, startIndex - overscanCount),
      Math.max(0, Math.min(itemCount - 1, stopIndex + overscanCount)),
      startIndex,
      stopIndex,
    ];
  }
}

正文完
 0