• reactwindow 源码浅析


    react-window

    这篇是 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,
                 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%',
               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/Grewer/react-virtualized-notes

    参考

  • 相关阅读:
    OCP-1Z0-053-V12.02-655题
    OCP-1Z0-053-V12.02-656题
    OCP-1Z0-053-V12.02-639题
    EXCEL文件打开缓慢的问题解决
    IOCP底层,支持超过15000个连接
    OCP-1Z0-053-V12.02-340题
    OCP-1Z0-053-V12.02-338题
    OCP-1Z0-053-V12.02-336题
    OCP-1Z0-053-V12.02-334题
    OCP-1Z0-053-V12.02-333题
  • 原文地址:https://www.cnblogs.com/Grewer/p/15948393.html
Copyright © 2020-2023  润新知