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

需要

实现一个固定尺寸的虚构渲染列表组件,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,    ];  }}// 计算区间开始索引,滚动间隔 除以 每个单元尺寸 就是  startIndexexport 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,    ];  }}