这篇是 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...