这篇是 react-window 的源码浏览, 因为此库应用的是 flow, 所以会波及一些非凡的货色, 相似 ts

应用

List

首先是 List 的应用:

import {FixedSizeList as List} from 'react-window';const Row = ({index, style}) => (    <div style={style}>Row {index}</div>);const App = () => (    <List        height={150}        itemCount={1000}        itemSize={35}        width={300}    >        {Row}    </List>);

绝对 react-virtual 的应用来说简略了很多, 使用方便, 然而绝对地, 裸露的也少了一点点

解析

首先它是在一整个 createListComponent 的根底上来创立 List 的具体方法的:

const FixedSizeList = createListComponent({    // ...    // 这里排列几个次要函数和他的具体作用})export default FixedSizeList;

这里先说下 createListComponent 的大体办法:

export default function createListComponent({                                                // 省略                                            }) {    return class List extends PureComponent {        // 滚动至 scrollOffset 的地位        scrollTo = (scrollOffset: number): void        // 滚动至某一 item 上, 通过传递对应序号        scrollToItem(index: number, align: ScrollToAlign = 'auto'): void        // 缓存参数        _callOnItemsRendered: (            overscanStartIndex: number,            overscanStopIndex: number,            visibleStartIndex: number,            visibleStopIndex: number        ) => void;        // 通过 index 来获取对应的style, 其中有, 长, 宽, left, top 等具体位置属性, 同时这些属性也有缓存        _getItemStyle: (index: number) => Object;        // 获取序号 ,   overscanStartIndex,overscanStopIndex, visibleStartIndex, visibleStopIndex        _getRangeToRender(): [number, number, number, number]        // 滚动时触发对应回调, 更新scrollOffset        _onScrollHorizontal = (event: ScrollEvent): void        // 同上        _onScrollVertical = (event: ScrollEvent): void        // 渲染函数        render() {        }    }}

createListComponent

上面咱们就详情的解析一下这个组件的办法:

export default function createListComponent({  getItemOffset,  getEstimatedTotalSize,  getItemSize,  getOffsetForIndexAndAlignment,  getStartIndexForOffset,  getStopIndexForStartIndex,  initInstanceProps,  shouldResetStyleCacheOnItemSizeChange,  validateProps,}) {    //间接就返回一个 class 组件, 没有闭包变量  return class List extends PureComponent {      //  初始化的时候获取的 props 参数    _instanceProps: any = initInstanceProps(this.props, this);    //内部元素 ref 对象    _outerRef: ?HTMLDivElement;    // 用来存取 定时器的    _resetIsScrollingTimeoutId: TimeoutID | null = null;    // 默认的参数    static defaultProps = {      direction: 'ltr', //  方向      itemData: undefined, // 每一个 item 的对象      layout: 'vertical', // 布局      overscanCount: 2, // 上部和下部超出的 item 个数      useIsScrolling: false, // 是否正在滚动    };    // 组件的 state    state: State = {      instance: this,      isScrolling: false,      scrollDirection: 'forward',      scrollOffset:        typeof this.props.initialScrollOffset === 'number'          ? this.props.initialScrollOffset          : 0, // 依据 props 来判断      scrollUpdateWasRequested: false,    };    // constructor    constructor(props: Props<T>) {      super(props);    }    //  props 到 state 的映射    static getDerivedStateFromProps(      nextProps: Props<T>,      prevState: State    ): $Shape<State> | null {        // 这个函数具体的源码咱们在上面阐明        // 对于 下一步收到的 props 和上一步的 state, 做出判断        // 如果收到的参数不标准则会报错, 能够疏忽      validateSharedProps(nextProps, prevState);      // validateProps 此办法是内部传递的, note 1      validateProps(nextProps);      return null;    }// 滚动至某一地位    scrollTo(scrollOffset: number): void {      // 确保 scrollOffset 大于 0      scrollOffset = Math.max(0, scrollOffset);      this.setState(prevState => {        // 同样地就 return        if (prevState.scrollOffset === scrollOffset) {          return null;        }        // 间接设置 scrollOffset        return {          // 滚动的方向          scrollDirection:            prevState.scrollOffset < scrollOffset ? 'forward' : 'backward',          scrollOffset: scrollOffset,          scrollUpdateWasRequested: true,        };        // 回调      }, this._resetIsScrollingDebounced);    }    // 办法同上, 作用是滚动至某一个 item 下面    scrollToItem(index: number, align: ScrollToAlign = 'auto'): void {      const { itemCount } = this.props;      const { scrollOffset } = this.state;      // 保障 index 在 0 和 item 最大值之间      index = Math.max(0, Math.min(index, itemCount - 1));      // 调用 scrollTo 办法, 参数是 getOffsetForIndexAndAlignment 的返回值      // 此函数作用是通过 index 获取对应 item 的偏移量, 最初通过偏移量滚动至对应的 item      // 函数通过  createListComponent 的传参获取, 不同的 list/grid, 可能有不必的计划      this.scrollTo(        getOffsetForIndexAndAlignment(          this.props,          index,          align,          scrollOffset,          this._instanceProps        )      );    }    // mount 所作的事件    componentDidMount() {      const { direction, initialScrollOffset, layout } = this.props;      // initialScrollOffset 是数字且 _outerRef 失常      if (typeof initialScrollOffset === 'number' && this._outerRef != null) {        const outerRef = ((this._outerRef: any): HTMLElement);        // TODO Deprecate direction "horizontal"        if (direction === 'horizontal' || layout === 'horizontal') {          outerRef.scrollLeft = initialScrollOffset;        } else {          outerRef.scrollTop = initialScrollOffset;        }      }      this._callPropsCallbacks();    }    componentDidUpdate() {      const { direction, layout } = this.props;      const { scrollOffset, scrollUpdateWasRequested } = this.state;      if (scrollUpdateWasRequested && this._outerRef != null) {        const outerRef = ((this._outerRef: any): HTMLElement); // outerRef能够说是最外层元素的 ref 对象        // 这里因为版本问题 可能还会去除  direction 的 horizontal 判断        if (direction === 'horizontal' || layout === 'horizontal') {          if (direction === 'rtl') {            // 针对不同的类型 来左右滚动至最 scrollOffset 的偏移量            switch (getRTLOffsetType()) {              case 'negative':                outerRef.scrollLeft = -scrollOffset;                break;              case 'positive-ascending':                outerRef.scrollLeft = scrollOffset;                break;              default:                const { clientWidth, scrollWidth } = outerRef;                outerRef.scrollLeft = scrollWidth - clientWidth - scrollOffset;                break;            }          } else {            outerRef.scrollLeft = scrollOffset;          }        } else {          // 针对高低的滚动          outerRef.scrollTop = scrollOffset;        }      }      // 调用此函数      // 作用是:  缓存节点, 滚动状态等数据      this._callPropsCallbacks();    }    // 组件来到时清空定时器    componentWillUnmount() {      if (this._resetIsScrollingTimeoutId !== null) {        cancelTimeout(this._resetIsScrollingTimeoutId);      }    }    // 渲染函数    render() {      const {        children,        className,        direction,        height,        innerRef,        innerElementType,        innerTagName,        itemCount,        itemData,        itemKey = defaultItemKey,        layout,        outerElementType,        outerTagName,        style,        useIsScrolling,        width,      } = this.props;      // 是否滚动      const { isScrolling } = this.state;      // direction "horizontal"  兼容老数据      const isHorizontal =        direction === 'horizontal' || layout === 'horizontal';      // 当滚动时的回调, 针对不同方向      const onScroll = isHorizontal        ? this._onScrollHorizontal        : this._onScrollVertical;      // 返回节点的范畴 [实在终点, 实在起点]      const [startIndex, stopIndex] = this._getRangeToRender();      const items = [];      if (itemCount > 0) {        // 循环所有 item 数来创立 item, createElement 传递参数        for (let index = startIndex; index <= stopIndex; index++) {          items.push(            createElement(children, {              data: itemData,              key: itemKey(index, itemData),              index,              isScrolling: useIsScrolling ? isScrolling : undefined,              style: this._getItemStyle(index), // render 时获取 style            })          );        }      }            // getEstimatedTotalSize来自 父函数 props      // 在我的项目被创立后读取这个值,因而它们的理论尺寸(如果是可变的)被思考在内      const estimatedTotalSize = getEstimatedTotalSize(        this.props,        this._instanceProps      );      // 动静, 可配置性地创立组件      return createElement(        outerElementType || outerTagName || 'div',        {          className,          onScroll,          ref: this._outerRefSetter,          style: {            position: 'relative',            height,            width,            overflow: 'auto',            WebkitOverflowScrolling: 'touch',            willChange: 'transform', // 提前优化, 相当于整体包装            direction,            ...style,          },        },        createElement(innerElementType || innerTagName || 'div', {          children: items,          ref: innerRef,          style: {            height: isHorizontal ? '100%' : estimatedTotalSize,            pointerEvents: isScrolling ? 'none' : undefined,            width: isHorizontal ? estimatedTotalSize : '100%',          },        })      );    }    _callOnItemsRendered: (      overscanStartIndex: number,      overscanStopIndex: number,      visibleStartIndex: number,      visibleStopIndex: number    ) => void;    // 作用 , 缓存最新的这四份数据    _callOnItemsRendered = memoizeOne(      (        overscanStartIndex: number,        overscanStopIndex: number,        visibleStartIndex: number,        visibleStopIndex: number      ) =>        ((this.props.onItemsRendered: any): onItemsRenderedCallback)({          overscanStartIndex,          overscanStopIndex,          visibleStartIndex,          visibleStopIndex,        })    );    // 缓存这 3 个数据    _callOnScroll: (      scrollDirection: ScrollDirection,      scrollOffset: number,      scrollUpdateWasRequested: boolean    ) => void;    _callOnScroll = memoizeOne(      (        scrollDirection: ScrollDirection,        scrollOffset: number,        scrollUpdateWasRequested: boolean      ) =>        ((this.props.onScroll: any): onScrollCallback)({          scrollDirection,          scrollOffset,          scrollUpdateWasRequested,        })    );    _callPropsCallbacks() {      // 判断来自 props 的 onItemsRendered是否是函数      if (typeof this.props.onItemsRendered === 'function') {        const { itemCount } = this.props;        if (itemCount > 0) {          // 总的数量大于 0 时          // 从_getRangeToRender获取节点的范畴          const [            overscanStartIndex, // 实在的终点            overscanStopIndex, // 实在的起点            visibleStartIndex, // 视图的终点            visibleStopIndex, // 视图的起点          ] = this._getRangeToRender();          // 调用 _callOnItemsRendered, 更新缓存          this._callOnItemsRendered(            overscanStartIndex,            overscanStopIndex,            visibleStartIndex,            visibleStopIndex          );        }      }      // 如果传递了 onScroll 函数过去      if (typeof this.props.onScroll === 'function') {        const {          scrollDirection,          scrollOffset,          scrollUpdateWasRequested,        } = this.state;        // 调用此函数, 作用同样是缓存数据        this._callOnScroll(          scrollDirection,          scrollOffset,          scrollUpdateWasRequested        );      }    }    // 在滚动时 lazy 地创立和缓存我的项目的款式,    // 这样 pure 组件的就能够避免从新渲染。    // 保护这个缓存,并传递一个props而不是index,    // 这样List就能够革除缓存的款式并在必要时强制从新渲染我的项目    _getItemStyle: (index: number) => Object;    _getItemStyle = (index: number): Object => {      const { direction, itemSize, layout } = this.props;      // 缓存 , itemSize, layout, direction 有扭转 也会造成缓存清空      const itemStyleCache = this._getItemStyleCache(        shouldResetStyleCacheOnItemSizeChange && itemSize,        shouldResetStyleCacheOnItemSizeChange && layout,        shouldResetStyleCacheOnItemSizeChange && direction      );      let style;      // 有缓存则取缓存, 留神 hasOwnProperty 和 in  [index] 的区别      if (itemStyleCache.hasOwnProperty(index)) {        style = itemStyleCache[index];      } else {        // getItemOffset 和 getItemSize 来自父函数 props        const offset = getItemOffset(this.props, index, this._instanceProps);        const size = getItemSize(this.props, index, this._instanceProps);        const isHorizontal =          direction === 'horizontal' || layout === 'horizontal';        const isRtl = direction === 'rtl';        const offsetHorizontal = isHorizontal ? offset : 0;        // 缓存 index:{} 至 itemStyleCache 对象        itemStyleCache[index] = style = {          position: 'absolute',          left: isRtl ? undefined : offsetHorizontal,          right: isRtl ? offsetHorizontal : undefined,          top: !isHorizontal ? offset : 0,          height: !isHorizontal ? size : '100%',          width: isHorizontal ? size : '100%',        };      }      return style;    };    _getItemStyleCache: (_: any, __: any, ___: any) => ItemStyleCache;    _getItemStyleCache = memoizeOne((_: any, __: any, ___: any) => ({}));    _getRangeToRender(): [number, number, number, number] {      // 数量相干数据      const { itemCount, overscanCount } = this.props;      // 是否滚动, 滚动方向, 滚动间隔      const { isScrolling, scrollDirection, scrollOffset } = this.state;      // 如果数量为 0  则 return      if (itemCount === 0) {        return [0, 0, 0, 0];      }      // 开始的x序号  getStartIndexForOffset 来源于 闭包传递, 通过间隔来获取序号       const startIndex = getStartIndexForOffset(        this.props,        scrollOffset,        this._instanceProps      );      // 完结的序号, 作用同上, 然而获取的是完结的序号      const stopIndex = getStopIndexForStartIndex(        this.props,        startIndex,        scrollOffset,        this._instanceProps      );      // 超出的范畴的数量, 前, 后 两个变量      const overscanBackward =        !isScrolling || scrollDirection === 'backward'          ? Math.max(1, overscanCount)          : 1;      const overscanForward =        !isScrolling || scrollDirection === 'forward'          ? Math.max(1, overscanCount)          : 1;      // 最终返回数据, [开始的节点序号-超出的节点,完结的节点序号+超出的节点, 开始的节点序号, 完结的节点序号]      return [        Math.max(0, startIndex - overscanBackward),        Math.max(0, Math.min(itemCount - 1, stopIndex + overscanForward)),        startIndex,        stopIndex,      ];    }    // 大体作用会和 _onScrollVertical 相似    _onScrollHorizontal = (event: ScrollEvent): void => {      const { clientWidth, scrollLeft, scrollWidth } = event.currentTarget;      this.setState(prevState => {        if (prevState.scrollOffset === scrollLeft) {          // 如果滚动间隔不变          return null;        }        const { direction } = this.props;        let scrollOffset = scrollLeft;        if (direction === 'rtl') {          // 依据方向确定滚动间隔          switch (getRTLOffsetType()) {            case 'negative':              scrollOffset = -scrollLeft;              break;            case 'positive-descending':              scrollOffset = scrollWidth - clientWidth - scrollLeft;              break;          }        }        // 保障间隔在范畴之内, 同时 Safari在越界时会有晃动        scrollOffset = Math.max(          0,          Math.min(scrollOffset, scrollWidth - clientWidth)        );        return {          isScrolling: true,          scrollDirection:            prevState.scrollOffset < scrollLeft ? 'forward' : 'backward',          scrollOffset,          scrollUpdateWasRequested: false,        };      }, this._resetIsScrollingDebounced);    };    // 同上 , 这里就不多说了    _onScrollVertical = (event: ScrollEvent): void => {      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 {          isScrolling: true,          scrollDirection:            prevState.scrollOffset < scrollOffset ? 'forward' : 'backward',          scrollOffset,          scrollUpdateWasRequested: false,        };      }, this._resetIsScrollingDebounced);    };    _outerRefSetter = (ref: any): void => {      const { outerRef } = this.props;      this._outerRef = ((ref: any): HTMLDivElement);      if (typeof outerRef === 'function') {        outerRef(ref);      } else if (        outerRef != null &&        typeof outerRef === 'object' &&        outerRef.hasOwnProperty('current')      ) {        outerRef.current = ref;      }    };    _resetIsScrollingDebounced = () => {      // 防止同一时间屡次调用 此函数, 起到一个节流的作用      if (this._resetIsScrollingTimeoutId !== null) {        cancelTimeout(this._resetIsScrollingTimeoutId);      }      // requestTimeout 是一个工具函数, 在提早 IS_SCROLLING_DEBOUNCE_INTERVAL = 150 ms 之后运行, 相似 setTimeout, 然而为什么不间接应用      // 引出额定的问题 setTimeout和requestAnimationFrame 的区别, 有趣味的能够自行理解      this._resetIsScrollingTimeoutId = requestTimeout(        this._resetIsScrolling,        IS_SCROLLING_DEBOUNCE_INTERVAL      );    };    _resetIsScrolling = () => {      // 执行的时候清空id      this._resetIsScrollingTimeoutId = null;      this.setState({ isScrolling: false }, () => {        // 在状态更新操作        // 防止isScrolling的影响        //  _getItemStyleCache 的具体作用, 他是一个通过 memoizeOne 过的函数        // 而 memoizeOne 是来源于`memoize-one`仓库 https://www.npmjs.com/package/memoize-one        // 用途是缓存最近的一个后果 而这里是返回一个空对象        // 在更新后清空 style        this._getItemStyleCache(-1, null);      });    };  };}

FixedSizeList

这个组件就是通过 createListComponent 来创立的最终后果:

const FixedSizeList = createListComponent({    // 前三个参数都非常简略,     getItemOffset: ({itemSize}: Props<any>, index: number): number =>        index * ((itemSize: any): number),    getItemSize: ({itemSize}: Props<any>, index: number): number =>        ((itemSize: any): number),    getEstimatedTotalSize: ({itemCount, itemSize}: Props<any>) =>        ((itemSize: any): number) * itemCount,    //  通过  index 算出 offset 间隔, 是一个比拟 pure 的计算函数  getOffsetForIndexAndAlignment: (    { direction, height, itemCount, itemSize, layout, width }: Props<any>,    index: number,    align: ScrollToAlign,    scrollOffset: number  ): number => {    const isHorizontal = direction === 'horizontal' || layout === 'horizontal';    const size = (((isHorizontal ? width : height): any): number);    const lastItemOffset = Math.max(      0,      itemCount * ((itemSize: any): number) - size    );    const maxOffset = Math.min(      lastItemOffset,      index * ((itemSize: any): number)    );    const minOffset = Math.max(      0,      index * ((itemSize: any): number) - size + ((itemSize: any): number)    );//  针对不同的 align 变量 做出不同应答    if (align === 'smart') {      if (        scrollOffset >= minOffset - size &&        scrollOffset <= maxOffset + size      ) {        align = 'auto';      } else {        align = 'center';      }    }    switch (align) {      case 'start':        return maxOffset;      case 'end':        return minOffset;      case 'center': {        const middleOffset = Math.round(          minOffset + (maxOffset - minOffset) / 2        );        if (middleOffset < Math.ceil(size / 2)) {          return 0; // 开始        } else if (middleOffset > lastItemOffset + Math.floor(size / 2)) {          return lastItemOffset;  //完结的地位        } else {          return middleOffset;        }      }      case 'auto':      default:        if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {          return scrollOffset;        } else if (scrollOffset < minOffset) {          return minOffset;        } else {          return maxOffset;        }    }  },  getStartIndexForOffset: (    { itemCount, itemSize }: Props<any>,    offset: number  ): number =>    Math.max(      0,      Math.min(itemCount - 1, Math.floor(offset / ((itemSize: any): number)))    ),  // 获取开始和完结的 index  getStopIndexForStartIndex: (    { direction, height, itemCount, itemSize, layout, width }: Props<any>,    startIndex: number,    scrollOffset: number  ): number => {    const isHorizontal = direction === 'horizontal' || layout === 'horizontal';    const offset = startIndex * ((itemSize: any): number);    const size = (((isHorizontal ? width : height): any): number);    const numVisibleItems = Math.ceil(      (size + scrollOffset - offset) / ((itemSize: any): number)    );    return Math.max(      0,      Math.min(        itemCount - 1,        startIndex + numVisibleItems - 1      )    );  },    // 默认空    initInstanceProps(props: Props<any>): any {        // Noop    },    // 是否在滚动结束后重置缓存    shouldResetStyleCacheOnItemSizeChange: true,    // 验证参数, 只在 dev 状况下有用估疏忽    validateProps: ({itemSize}: Props<any>): void => {    },});

通过后面 List demo 级别的调用, 咱们就很容易来创立一个简略的虚构列表

扩大点

FixedSizeList 只是一种简略的虚构列表状况 在 react-window 中还会适配以下状况

  • VariableSizeList 可适配不同 item 的高度(宽度)的状况, 然而须要传递一个参数来给予信息
  • FixedSizeGrid 反对双向的滚动, 荷香纵向都是虚构列表, 这种状况在 table 里可能会多一点
  • VariableSizeGrid 不同高度(宽度)的双向滚动虚构列表

原理都是大同小异, 这里就不过多阐明

参考

  • https://github.com/bvaughn/re...