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大小以及所有列表元素的位置,计算在可视区应该渲染哪些元素。
具体实现步骤如下所示:
- 首先确定长列表所在父元素的大小,父元素的大小决定了可视区的宽和高
- 确定长列表每一个列表元素的宽和高,同时初始的条件下计算好长列表每一个元素相对于父元素的位置,并用一个数组来保存所有列表元素的位置信息
- 首次渲染时,只展示相对于父元素可视区内的子列表元素,在滚动时,根据父元素的滚动的offset重新计算应该在可视区内的子列表元素。这样保证了无论如何滚动,真实渲染出的dom节点只有可视区内的列表元素。
- 假设可视区内能展示5个子列表元素,及时长列表总共有1000个元素,但是每时每刻,真实渲染出来的dom节点只有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>
}
/>
}
}
效果为:
该组件中通过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。