react-window 是什么?
react-window其实就是一个利用虚拟列表技术来实现性能优化的npm包,可以简单的理解为如果你有十万条数据需要渲染,如果不考虑性能可以一次性的渲染出来。 但是大多数情况下,我们的屏幕的可视区域其实根本无法完全展示这十万条数据,所以大部分数据其实都是在可视区域外面,也就是类似隐藏在深海下,我们根本看不到的,所以其实我们也没有必要渲染,而是只渲染我们可以看到的可视区域里边的数据就可以了。
react-window其实就是按照这个原理实现的。
react-window 源码
react-window是使用flow检查类型的,至于对于flow不是很熟悉也没什么关系,基本上和typescript差不多。
FixedSizeList
FixedSizeList使用
FixedSizeList 是最简单,首先来看下如何使用
import {FixedSizeList as List} from 'react-window';
const Row = ({index, style}) => (
<div style={style}>Row {index}</div>
);
const App = () => (
<List
// height是最外层div渲染的高度
height={150}
// 总条数
itemCount={1000}
// 每条的高度
itemSize={35}
// width是最外层div的宽度
width={300}
>
{Row}
</List>
);
FixedSizeList实现
我们已经看过FixedSizeList的声明式使用了,那么我们就可以带着问题看下react-window是如何实现的。
- 假如是你来实现类似的功能,你会如何实现?
假如是我,可能会采用类似架构
- react-window 会如何渲染什么样的结构?
可以看下源码里边,代码其实很简单,就是调用createElement,创建出合适的dom结构。
容器div绑定onscroll事件,在容器div里边再添加一个Inner div,作为内部元素的容器
return createElement(
outerElementType || outerTagName || 'div',
{
className,
// 绑定onScroll事件
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%',
},
})
);
- 我们通过监听onScroll事件,那么当onScroll事件发生后,我们是如何确定渲染那些数据的呢?
其实也很简单,其实就是我们在监听到onScroll事件的时候,获取到滚动的距离scrollTop之后,然后更新state中的offset状态。
代码如下:
// 不相关的代码已经删掉
_onScrollVertical = (event: ScrollEvent): void => {
const { clientHeight, scrollHeight, scrollTop } = event.currentTarget;
this.setState(prevState => {
// Prevent Safari's elastic scrolling from causing visual shaking when scrolling past bounds.
const scrollOffset = Math.max(
0,
Math.min(scrollTop, scrollHeight - clientHeight)
);
return {
isScrolling: true,
scrollDirection:
prevState.scrollOffset < scrollOffset ? 'forward' : 'backward',
scrollOffset,
scrollUpdateWasRequested: false,
};
}, this._resetIsScrollingDebounced);
};
触发更新之后,会重新计算startIndex和stopIndex,重新渲染
重新计算函数如下:
// 这里计算出需要渲染数据的起始和结束
_getRangeToRender(): [number, number, number, number] {
// itemCount 总条数,overscanCount表示多加载几条,就是除了可视区域之外,多加载几条
const { itemCount, overscanCount } = this.props;
const { isScrolling, scrollDirection, scrollOffset } = this.state;
// 如果总数据是0
if (itemCount === 0) {
return [0, 0, 0, 0];
}
// getStartIndexForOffset这里是外面传入进来的
const startIndex = getStartIndexForOffset(
this.props,
scrollOffset,
this._instanceProps
);
// getStopIndexForStartIndex 也是外面传入进来的
const stopIndex = getStopIndexForStartIndex(
this.props,
startIndex,
scrollOffset,
this._instanceProps
);
return [
Math.max(0, startIndex - overscanBackward),
Math.max(0, Math.min(itemCount - 1, stopIndex + overscanForward)),
startIndex,
stopIndex,
];
}
可以看到代码很简单,就是通过外部传入的getStartIndexForOffset和getStopIndexForStartIndex,重新计算出startIndex和stopIndex就可以了 现在让我们来看下这两个函数 getStartIndexForOffset
// 根据滚动的位置计算起始索引
getStartIndexForOffset: (
{ itemCount, itemSize }: Props<any>,
offset: number
): number =>
Math.max(
0,
Math.min(itemCount - 1, Math.floor(offset / ((itemSize: any): number)))
),
可以看到很简单,就是根据滚动的距离重新计算出初始值
然后我们再来看下getStopIndexForStartIndex
// 计算结束索引
getStopIndexForStartIndex: (
{ direction, height, itemCount, itemSize, layout, width }: Props<any>,
startIndex: number,
scrollOffset: number
): number => {
// TODO Deprecate direction "horizontal"
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 // -1 is because stop index is inclusive
)
);
},
这里和getStartIndexForOffset不同的就是我们把height加入进去了,其他的计算没什么区别
总结
我们只是分析了FixedSizeList,至于其它的VariableSizeGrid等其实原理差不多的,所以只需要搞懂FixedSizeList原理,其它都很简单了。
react-window 里边并没有对于onScroll进行防抖处理,如果涉及快速下拉就会涉及到大量计算,这些应该都可以优化。
而且前面计算过的startIndex和stopIndex应该也都可以进行缓存复用。