forthealllight/blog

react-tiny-virtual-list源码阅读

forthealllight opened this issue · 0 comments

React-tiny-virtual-list源码阅读


  在上一章介绍了如何在React,通过虚拟列表的形式优化长列表。介绍了虚拟列表的原理,以及比较常用的优化长列表的组件库React-virtualized和React-tiny-virtual-list。本文中来读一读React-tiny-virtual-list源码。

  • 虚拟列表的原理简介
  • React-tiny-virtual-list组件的使用
  • React-tiny-virtual-list的源码分析
  • React-tiny-virtual-list的总结

一、虚拟列表原理简介

优化长列表的原理很简单,基本原理可以一句话概括:

用数组保存所有列表元素的位置,只渲染可视区内的列表元素,当可视区滚动时,根据滚动的offset大小以及所有列表元素的位置,计算在可视区应该渲染哪些元素。

具体实现步骤如下所示:

  1. 首先确定长列表所在父元素的大小,父元素的大小决定了可视区的宽和高
  2. 确定长列表每一个列表元素的宽和高,同时初始的条件下计算好长列表每一个元素相对于父元素的位置,并用一个数组来保存所有列表元素的位置信息
  3. 首次渲染时,只展示相对于父元素可视区内的子列表元素,在滚动时,根据父元素的滚动的offset重新计算应该在可视区内的子列表元素。这样保证了无论如何滚动,真实渲染出的dom节点只有可视区内的列表元素。
  4. 假设可视区内能展示5个子列表元素,及时长列表总共有1000个元素,但是每时每刻,真实渲染出来的dom节点只有5个。
  5. 补充说明,这种情况下,父元素一般使用position:relative,子元素的定位一般使用:position:absolute或sticky

  通过虚拟列表的方式,不需要同时渲染很多dom节点,只需要渲染出可视区的dom节点,这种方式可以大大减小渲染时间,提升用户体验。

二、React-tiny-virtual-list组件的使用

  React-tiny-virtual-list是一个极简的React虚拟列表优化组件库,我们来看如何使用这个组件:

import VirtualList from 'react-tiny-virtual-list';
const data = ['A', 'B', 'C', 'D', 'E',   'F','G','H','I','J','K','L'];
class TinyVirtual extends Component {
  render(){
    return <VirtualList
            width={"100%"}
            height={200}
            itemCount={data.length}
            itemSize={50}
            renderItem={({index, style}) =>
              <div key={index} style={style}>
                 The style property contains the item's absolute position Letter: {data[index]}, Row: #{index}
              </div>
            }
            />
  }
}

效果为:

911543739688_ pic_hd

  该组件中通过width和height分别定义了宽度和高度,通过itemCount定义了所有需要被渲染成dom节点的数据数组的长度。itemSize定义了渲染的每一个dom的高度,而renderItem定义了如何结合数据来渲染一个dom。

  此外react-tiny-virtual-list的List组件还有scrollDirection属性决定是垂直滚动还是水平滚动,以及recomputeSizes函数用于重新计算列表中每一个dom元素的高度等。

三、React-tiny-virtual-list的源码分析

  下面我们来根据常用的width、height、itemCount、itemSize和renderItem来分析React-tiny-virtual-list的源码。React-tiny-virtual-list的源码包含了一个VirtualList组件,以及处理关于保存了VirtualList组件所渲染的列表元素的位置,同时决定其能否在可视区显示的工具类——SizeAndPositionManager类。

下面来一一分析:

(1)、VirtualList的初始化和render方法

首先来看这个VirtualList组件初始化和render的方法中做了哪些事情,在初始化VirtualList组件时:

itemSizeGetter = (itemSize) => {
  return index => this.getSize(index, itemSize);
};

sizeAndPositionManager = new SizeAndPositionManager({
    itemCount: this.props.itemCount,
    itemSizeGetter: this.itemSizeGetter(this.props.itemSize),
    estimatedItemSize: this.getEstimatedItemSize(),
});

state = {
  offset:
      this.props.scrollOffset ||
      (this.props.scrollToIndex != null &&
        this.getOffsetForIndex(this.props.scrollToIndex)) ||
      0,
 scrollChangeReason: SCROLL_CHANGE_REASON.REQUESTED,
};

在初始化的时候,在state中定义并赋值了整个列表应该滚动到哪一个位置的offset属性,可以根据该值来决定整个列表中,哪几个列dom需要在可视区内被渲染。

并且根据虚拟列表的原理我们指导,需要在内存中保存列表中每一个dom元素的相对于父元素的位置等信息,并且在父元素滚动等操作的时候需要更新子元素列的位置信息。在这里我们通过一个SizeAndPositionManager类来实现。

首先看这个类的状态定义:

class SizeAndPositionManager {
  constructor({itemCount, itemSizeGetter, estimatedItemSize}: Options) {
    this.itemSizeGetter = itemSizeGetter;
    this.itemCount = itemCount;
    this.estimatedItemSize = estimatedItemSize;
    this.itemSizeAndPositionData = {};
    this.lastMeasuredIndex = -1;
}

这个类中有itemSizeGetter方法用于获取从props传入的每一个子元素size的值,以及列表总列数等信息。itemSizeAndPositionData对象用户保存每一列的大小和相对于父元素的定位信息。lastMeasuredIndex表示可视区内最后一个元素的索引。

此外在VirtualList组件的初始化中还有:

styleCache= {};

用于在内存中保存所有子元素的样式,通过index索引。初始化结束后,再来看render的过程:

render() {
    const {
      estimatedItemSize,
      height,
      ...props
    } = this.props;
    const {offset} = this.state;//保存在当前的父元素中,相对于顶部父亲元素滚动的举例offset
    const {start, stop} = this.sizeAndPositionManager.getVisibleRange({
      containerSize: this.props[sizeProp[scrollDirection]] || 0,
      offset,
      overscanCount,
    }); //根据父元素滚动的距离,以及父元素的高度等信息,来决定父元素可视区内应该展示哪几个子元素
    const items: React.ReactNode[] = []; //保存应该在可视区内被渲染的子列表数组
    //找出应该在可视区内被渲染的子列表元素的index之后,就需要通过this.getStyle方法来获取该元素的样式以及在父元素汇总的距离。获取渲染元素的索引值,以及样式之后,通过在属性中通过props传入的renderItem方法,构建应该渲染出的列表元素。
    if (typeof start !== 'undefined' && typeof stop !== 'undefined') {
    for (let index = start; index <= stop; index++) {
        items.push(
          renderItem({
            index,
            style: this.getStyle(index, false),
          }),
        );
    }
  
   return (
     <div ref={this.getRef} {...props} style={wrapperStyle}>
      <div style={innerStyle}>{items}</div>
    </div>
  );
}

其中需要在于this.getStyle方法:

getStyle(index: number, sticky: boolean) {

    const style = this.styleCache[index];

    if (style) {
      return style;
    }

    const {scrollDirection = DIRECTION.VERTICAL} = this.props;
    const {
      size,
      offset,
    } = this.sizeAndPositionManager.getSizeAndPositionForIndex(index);

    return (this.styleCache[index] = sticky
      ? {
          ...STYLE_STICKY_ITEM,
          [sizeProp[scrollDirection]]: size,
          [marginProp[scrollDirection]]: offset,
          [oppositeMarginProp[scrollDirection]]: -(offset + size),
          zIndex: 1,
        }
      : {
          ...STYLE_ITEM,
          [sizeProp[scrollDirection]]: size,
          [positionProp[scrollDirection]]: offset,
        });
}

首先从styleCache的内存中去查找,如果找到就直接返回,否则就根据在初始的时候定义的this.sizeAndPositionManager的getSizeAndPositionForIndex中根据索引来得到响应元素的大小,以及相对于父元素的位置,并以index为索引保存在styleCache内存中。

此外,在render的return中,我们通过ref的方式在virtual dom中获取了父元素。

getRef = (node) => {
  this.rootNode = node;
};

(2)、VirtualList组件处理滚动事件

在VirtualList组件的componentDidMount的声明周期中开始监听滚动事件:

const {scrollOffset, scrollToIndex} = this.props;
this.rootNode.addEventListener('scroll', this.handleScroll, {
  passive: true,
});

接着来看处理滚动事件的函数this.handleScroll:

handleScroll = (event) => {
    const {onScroll} = this.props;
    const offset = this.getNodeOffset();

    if (
      offset < 0 ||
      this.state.offset === offset ||
      event.target !== this.rootNode
    ) {
      return;
    }

    this.setState({
      offset,
      scrollChangeReason: SCROLL_CHANGE_REASON.OBSERVED,
    });

    if (typeof onScroll === 'function') {
      onScroll(offset, event);
    }
};

getNodeOffset() {
  const {scrollDirection = DIRECTION.VERTICAL} = this.props;
  return this.rootNode[scrollProp[scrollDirection]];
}

在这个handleScroll事件中,通过getNodeOffset方法可以获得滚动后父元素滚动的高度,然后在this.setState方法中更新这个offset的值。只要state中的offset的值发生变化,就会重新触发render,在render中根据新的offset来确定可视区应该显示哪些列表。

四、React-tiny-virtual-list总结

  • 从源码可以看出来React-tiny-virtual-list的组件是不支持动态高度的,也就是做子元素必须是指定的固定高度,否则随着父元素的滚动,可视区内应该展示哪些子元素会不准确。主要缺陷在于在滚动过程中,没有区重新计算子元素的高度。
  • 其次,在源码中我们也可以看出来,其实React-tiny-virtual-list并没有复用可视区内的dom。