☞閱讀原文
考考你
問:在數據項高度不確定情況下,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]