☞阅读原文
考考你
问:在数据项高度不确定情况下,js侧不具备直接计算组件大小的能力,是怎么知道首屏展示几个数据项?
答:js侧首屏几个不是直接计算出来的,而是先通过设置的属性估算出几个数据项,同时设置数据项和列表的布局监听回调onLayout,回调中修正数据项个数(如果还有数据项并且屏幕还有空间,则继续添加数据项)。问:FlatList只展示屏幕中的个数,那怎么还能快速滚动,Native底层对应的可是普通的滚动容器ScrollView?
答:FlatList中数据项个数不止屏幕中显示的个数,会有超出屏幕外的多屏数据项(通过windowSize属性设置)。假如实际数据项100,FlatList一屏能展示10个,此时FlatList中的个数可能是30个,如果你快速滑动,只能滑出30个就到底了,需要过一会才发现还能接着滑动。问:FlatList中空白项和实际未显示的数据项个数对应么,空白项高度怎么计算?
答:空白项和实际未显示个数不对应,最多只有两个空白项(顶部空白项和底部空白项)。如果未显示数据项高度已经计算过(之前在界面显示过),则直接累加即可算出,如果位置,则通过估算(已知数据项高度的平均值)。问:FlatList滑动起来一点都不卡,必要的计算还是有的,这是怎么做到的?
答:除少数情况下(用户滑动到的页面是空白),计算工作大部分在InteractionManager.runAfterInteractions(将一些耗时较长的工作安排到所有互动或动画完成之后再进行)执行。有效的避免了和用户交互抢占CPU,而且是用户交互停下来了才触发,避免了跟随用户动作频繁计算。不过弊端就是上面的少数情况下会卡顿(预设的缓冲用完了,只能强制在交互中同步计算)或者看起来像彩蛋(快速滑动还没有显示完所有数据就到底滑不动了、快速滑动下白屏)。问:网上说FlatList的原理是“不在屏幕中的组件会被移除,通过空白替代”,和没说一样?
答:
1. 长列表最大的硬伤是随着列表不断滑动,数据项越来越多,内存越来越大,然后就OOM了。通过将屏幕外组件移除是解决该硬伤的核心思想(其实核心思想就这么几种,大家都能想到,困惑的其实是怎么做到的)。
2. FlatList首先会预缓冲很多屏数据,这样不会影响正常显示和滑动功能。其次就是在互动或动画结束后再刷新缓冲区域,这样不会卡。再次通过key保证了缓冲区是增量刷新,并且限制增量大小,确保不会卡。问:getItemLayout真能提高性能么?
答:getItemLayout能直接获取数据项控件位置和大小,无需借助onLayout回调,可以提高位置计算效率。问:多级吸顶怎么做的?
答:实际使用的是ScrollView.js中自带的吸顶功能(通过位移动画实现)。FlatList虽然声明的属性没有说支持吸顶,但通过设置隐藏属性stickyHeaderIndices(这个属性在VirtualizedList.js里面用到,但是没有显示声明,FlatList会将自身所有属性直接赋值给VirtualizedList)能支持吸顶。
怎么看
- 直接看源码,功力不够,也没有这个耐心,这和看英文文章一样,看着看着就(~﹃~)~zZ。
- 直接打断点一步步Debug,随便一个操作会让你Debug到停不下来,直到怀疑人生。
- 首先网上搜一下相关文章,熟悉一下大概。其次从核心入口render方法大概看看,找到一些关键函数。再次就是直接打日志(js代码就是这个好,依赖文件直接加日志就可以跑),串一下思路。再再次就是日志串不起来再再回头断点看看,来来回回你就懂了。
剖析
- 整个Demo
export default class App extends Component<Props> {
renderItem = (item) => {
var txt = '第' + item.index + '个' + ' title=' + item.item.title;
var bgColor = item.index % 2 == 0 ? 'red' : 'blue';
return <Text style={[{flex: 1, height: 100, backgroundColor: bgColor}, styles.txt]}>{txt}</Text>
}
render() {
var data = [];
for (var i = 0; i < 1000; i++) {
data.push({key: i, title: i + ''});
}
return (
<View style={{flex: 1}}>
<View style={{flex: 1}}>
<FlatList
initialNumToRender={1}
windowSize={2}
renderItem={this.renderItem}
data={data}>
</FlatList>
</View>
</View>
);
}
}
-
长这样
-
加点日志
-
VirtualizedList.js
- console.log('SSU', '\n\nVirtualizedList#render(){列表开始渲染}\n\n', this.state);
- console.log('SSU', 'VirtualizedList#render()$lead_spacer{列表添加头部空白块}',{lastInitialIndex, initBlock, first, firstBlock}, {[spacerKey]: firstSpace});
- console.log('SSU', 'VirtualizedList#render()$tail_spacer{列表添加尾部空白块}', {last, lastFrame, end, endFrame},{[spacerKey]: tailSpacerLength})
- console.log('SSU', 'VirtualizedList#_pushCells(){填充列表项}',{first, last}, cells);
- console.log('SSU', 'VirtualizedList#_onCellLayout(){开始列表项布局回调}', cellKey, index);
- console.log('SSU', 'VirtualizedList#onCellLayout(){列表项布局回调结束}', next, this.frames);
- console.log('SSU', 'VirtualizedList#_onLayout(){列表布局回调}', e.nativeEvent.layout);
- console.log('SSU', 'VirtualizedList#onLayout(){列表布局回调修正滚动参数}this.scrollMetrics.visibleLength=', this._scrollMetrics.visibleLength);
- console.log('SSU', 'VirtualizedList#onContentSizeChange(){列表内容请大小变化回调}', width, height, this.scrollMetrics);
- console.log('SSU', 'VirtualizedList#_onScroll(){滚动开始}', e.nativeEvent.layoutMeasurement, e.nativeEvent.contentSize, e.nativeEvent.contentOffset);
- console.log('SSU', 'VirtualizedList#onScroll(){滚动修正滚动参数}', this.scrollMetrics);
- console.log('SSU', 'VirtualizedList#_onScroll(){滚动结束}');
- console.log('SSU', 'VirtualizedList#_scheduleCellsToRenderUpdate(){安排列表项更新优先级}', {first, last}, {offset, visibleLength, velocity}, itemCount);
- console.log('SSU', 'VirtualizedList#_scheduleCellsToRenderUpdate(){优先级高,直接更新列表项}', {hiPri, _averageCellLength: this._averageCellLength, hiPriInProgress: this.hiPriInProgress});
- console.log('SSU', 'VirtualizedList#_scheduleCellsToRenderUpdate(){优先级低,等空闲再更新列表项}', {hiPri, _averageCellLength: this._averageCellLength, hiPriInProgress: this.hiPriInProgress});
-
Batchinator.js
- console.log('SSU', 'Batchinator#schedule(){安排列表项更新}');
- console.log('SSU', 'Batchinator#schedule()InteractionManager.runAfterInteractions(){开始执行列表项更新}');
- console.log('SSU', 'Batchinator#schedule()InteractionManager.runAfterInteractions(){结束执行列表项更新}');
-
VirtualizeUtils.js
- console.log('SSU', 'VirtualizeUtils#computeWindowedRenderLimits(){开始计算屏幕渲染列表项区域}', {maxToRenderPerBatch, windowSize}, prev, scrollMetrics);
- console.log('SSU', 'VirtualizeUtils#computeWindowedRenderLimits(){计算屏幕渲染列表项区域}', {visibleBegin, visibleEnd}, {overscanLength, fillPreference, overscanBegin, overscanEnd});
- console.log('SSU', 'VirtualizeUtils#computeWindowedRenderLimits(){完成计算屏幕渲染列表项区域}', prev, {first, last});
- console.log('SSU', 'VirtualizeUtils#computeWindowedRenderLimits(){完成计算屏幕渲染列表项区域}', prev, {first, last}, {overscanFirst, overscanLast, newCellCount});
-
初始化显示
-
根据设置参数预估显示数据项区[0,1](不用担心是否显示一屏,上述Demo初始数据项个数为1)
数据项布局变化、列表布局变化、列表内容区大小变化均会安排列表项更新(显示数据项个数)优先级
根据数据项返回高度、列表高度、列表内容区大小计算出显示不满一屏,直接更新列表项
计算出当前状态下计算出显示列表项区间[0,8],通过setState触发重新render
-
render出9个数据项和1个尾部空白区,接着走2,因为此时一屏可以显示下,优先级低,所以等待空闲触发更新列表项
-
计算出当前状态下不需要继续添加数据项,setState没有变化,更新停止,页面状态稳定
-
-
滚动显示(用力向下滑动,发现滑动到“第8个 title=8”就再也划不动了,过一会又可以接着滑)
[图片上传失败...(image-a897f6-1563364857791)]- 滚动回调(_onScroll)会安排列表项更新(显示数据项个数)优先级,优先级低,所以等待空闲触发更新列表项
- 等到滑动到最后一个列表项时,滑不动了,此时空闲,触发更新列表项
- 根据当前状态,计算出显示列表项区间[0,11]
- render出12个数据项和1个尾部空白区,等待空闲更新列表项
- 列表项计算区间没有变化,更新停止,页面状态稳定
-
不断向下滑动,找到一个中间状态(比如第一项显示“第62个 title=62”发现有顶部空白和底部空白)
-
快速向上滑动,发现会有空白块闪烁一下后再显示出对应内容,滑动流畅
尝试设置getItemLayout属性,发现不再有数据项的布局回调,而且空白区的高度准确(不会出现滑动到一定位置就滑不动的彩蛋)。这么看好像性能也没有提高多少。
[图片上传失败...(image-28ff22-1563364857791)]
-
原理
- 整个过程就是多render几次,然后就达到平衡态了。
[https://user-gold-cdn.xitu.io/2019/7/17/16bffc9c0d5ed14b?imageView2/0/w/1280/h/960/format/webp/ignore-error/1] - 整个过程有个窗口的概念,通过一通计算,得到当前状态一个合适的窗口大小。
[https://user-gold-cdn.xitu.io/2019/7/17/16bffc9cbef91719?imageView2/0/w/1280/h/960/format/webp/ignore-error/1] - 各个文件的依赖关系。整体看一下基本就拼接成现在的功能,核心在VirtualizedList。
[https://user-gold-cdn.xitu.io/2019/7/17/16bffc9d13784962?imageView2/0/w/1280/h/960/format/webp/ignore-error/1]