进击ReactNative-FlatList源码解析

阅读原文

考考你

  1. 问:在数据项高度不确定情况下,js侧不具备直接计算组件大小的能力,是怎么知道首屏展示几个数据项?
    答:js侧首屏几个不是直接计算出来的,而是先通过设置的属性估算出几个数据项,同时设置数据项和列表的布局监听回调onLayout,回调中修正数据项个数(如果还有数据项并且屏幕还有空间,则继续添加数据项)。

  2. 问:FlatList只展示屏幕中的个数,那怎么还能快速滚动,Native底层对应的可是普通的滚动容器ScrollView?
    答:FlatList中数据项个数不止屏幕中显示的个数,会有超出屏幕外的多屏数据项(通过windowSize属性设置)。假如实际数据项100,FlatList一屏能展示10个,此时FlatList中的个数可能是30个,如果你快速滑动,只能滑出30个就到底了,需要过一会才发现还能接着滑动。

  3. 问:FlatList中空白项和实际未显示的数据项个数对应么,空白项高度怎么计算?
    答:空白项和实际未显示个数不对应,最多只有两个空白项(顶部空白项和底部空白项)。如果未显示数据项高度已经计算过(之前在界面显示过),则直接累加即可算出,如果位置,则通过估算(已知数据项高度的平均值)。

  4. 问:FlatList滑动起来一点都不卡,必要的计算还是有的,这是怎么做到的?
    答:除少数情况下(用户滑动到的页面是空白),计算工作大部分在InteractionManager.runAfterInteractions(将一些耗时较长的工作安排到所有互动或动画完成之后再进行)执行。有效的避免了和用户交互抢占CPU,而且是用户交互停下来了才触发,避免了跟随用户动作频繁计算。不过弊端就是上面的少数情况下会卡顿(预设的缓冲用完了,只能强制在交互中同步计算)或者看起来像彩蛋(快速滑动还没有显示完所有数据就到底滑不动了、快速滑动下白屏)。

  5. 问:网上说FlatList的原理是“不在屏幕中的组件会被移除,通过空白替代”,和没说一样?
    答:
    1. 长列表最大的硬伤是随着列表不断滑动,数据项越来越多,内存越来越大,然后就OOM了。通过将屏幕外组件移除是解决该硬伤的核心思想(其实核心思想就这么几种,大家都能想到,困惑的其实是怎么做到的)。
    2. FlatList首先会预缓冲很多屏数据,这样不会影响正常显示和滑动功能。其次就是在互动或动画结束后再刷新缓冲区域,这样不会卡。再次通过key保证了缓冲区是增量刷新,并且限制增量大小,确保不会卡。

  6. 问:getItemLayout真能提高性能么?
    答:getItemLayout能直接获取数据项控件位置和大小,无需借助onLayout回调,可以提高位置计算效率。

  7. 问:多级吸顶怎么做的?
    答:实际使用的是ScrollView.js中自带的吸顶功能(通过位移动画实现)。FlatList虽然声明的属性没有说支持吸顶,但通过设置隐藏属性stickyHeaderIndices(这个属性在VirtualizedList.js里面用到,但是没有显示声明,FlatList会将自身所有属性直接赋值给VirtualizedList)能支持吸顶。

怎么看

  1. 直接看源码,功力不够,也没有这个耐心,这和看英文文章一样,看着看着就(~﹃~)~zZ。
  2. 直接打断点一步步Debug,随便一个操作会让你Debug到停不下来,直到怀疑人生。
  3. 首先网上搜一下相关文章,熟悉一下大概。其次从核心入口render方法大概看看,找到一些关键函数。再次就是直接打日志(js代码就是这个好,依赖文件直接加日志就可以跑),串一下思路。再再次就是日志串不起来再再回头断点看看,来来回回你就懂了。

剖析

  1. 整个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>
        );
    }
}
  1. 长这样


    demo
  2. 加点日志

    1. VirtualizedList.js

      1. console.log('SSU', '\n\nVirtualizedList#render(){列表开始渲染}\n\n', this.state);
      2. console.log('SSU', 'VirtualizedList#render()$lead_spacer{列表添加头部空白块}',{lastInitialIndex, initBlock, first, firstBlock}, {[spacerKey]: firstSpace});
      3. console.log('SSU', 'VirtualizedList#render()$tail_spacer{列表添加尾部空白块}', {last, lastFrame, end, endFrame},{[spacerKey]: tailSpacerLength})
      4. console.log('SSU', 'VirtualizedList#_pushCells(){填充列表项}',{first, last}, cells);
      5. console.log('SSU', 'VirtualizedList#_onCellLayout(){开始列表项布局回调}', cellKey, index);
      6. console.log('SSU', 'VirtualizedList#onCellLayout(){列表项布局回调结束}', next, this.frames);
      7. console.log('SSU', 'VirtualizedList#_onLayout(){列表布局回调}', e.nativeEvent.layout);
      8. console.log('SSU', 'VirtualizedList#onLayout(){列表布局回调修正滚动参数}this.scrollMetrics.visibleLength=', this._scrollMetrics.visibleLength);
      9. console.log('SSU', 'VirtualizedList#onContentSizeChange(){列表内容请大小变化回调}', width, height, this.scrollMetrics);
      10. console.log('SSU', 'VirtualizedList#_onScroll(){滚动开始}', e.nativeEvent.layoutMeasurement, e.nativeEvent.contentSize, e.nativeEvent.contentOffset);
      11. console.log('SSU', 'VirtualizedList#onScroll(){滚动修正滚动参数}', this.scrollMetrics);
      12. console.log('SSU', 'VirtualizedList#_onScroll(){滚动结束}');
      13. console.log('SSU', 'VirtualizedList#_scheduleCellsToRenderUpdate(){安排列表项更新优先级}', {first, last}, {offset, visibleLength, velocity}, itemCount);
      14. console.log('SSU', 'VirtualizedList#_scheduleCellsToRenderUpdate(){优先级高,直接更新列表项}', {hiPri, _averageCellLength: this._averageCellLength, hiPriInProgress: this.hiPriInProgress});
      15. console.log('SSU', 'VirtualizedList#_scheduleCellsToRenderUpdate(){优先级低,等空闲再更新列表项}', {hiPri, _averageCellLength: this._averageCellLength, hiPriInProgress: this.hiPriInProgress});
    2. Batchinator.js

      1. console.log('SSU', 'Batchinator#schedule(){安排列表项更新}');
      2. console.log('SSU', 'Batchinator#schedule()InteractionManager.runAfterInteractions(){开始执行列表项更新}');
      3. console.log('SSU', 'Batchinator#schedule()InteractionManager.runAfterInteractions(){结束执行列表项更新}');
    3. VirtualizeUtils.js

      1. console.log('SSU', 'VirtualizeUtils#computeWindowedRenderLimits(){开始计算屏幕渲染列表项区域}', {maxToRenderPerBatch, windowSize}, prev, scrollMetrics);
      2. console.log('SSU', 'VirtualizeUtils#computeWindowedRenderLimits(){计算屏幕渲染列表项区域}', {visibleBegin, visibleEnd}, {overscanLength, fillPreference, overscanBegin, overscanEnd});
      3. console.log('SSU', 'VirtualizeUtils#computeWindowedRenderLimits(){完成计算屏幕渲染列表项区域}', prev, {first, last});
      4. console.log('SSU', 'VirtualizeUtils#computeWindowedRenderLimits(){完成计算屏幕渲染列表项区域}', prev, {first, last}, {overscanFirst, overscanLast, newCellCount});
    4. 初始化显示

      1. 根据设置参数预估显示数据项区[0,1](不用担心是否显示一屏,上述Demo初始数据项个数为1)


        image
        image
      2. 数据项布局变化、列表布局变化、列表内容区大小变化均会安排列表项更新(显示数据项个数)优先级

      3. 根据数据项返回高度、列表高度、列表内容区大小计算出显示不满一屏,直接更新列表项

      4. 计算出当前状态下计算出显示列表项区间[0,8],通过setState触发重新render

      5. render出9个数据项和1个尾部空白区,接着走2,因为此时一屏可以显示下,优先级低,所以等待空闲触发更新列表项


        image
      6. 计算出当前状态下不需要继续添加数据项,setState没有变化,更新停止,页面状态稳定


        image
    5. 滚动显示(用力向下滑动,发现滑动到“第8个 title=8”就再也划不动了,过一会又可以接着滑)
      [图片上传失败...(image-a897f6-1563364857791)]

      1. 滚动回调(_onScroll)会安排列表项更新(显示数据项个数)优先级,优先级低,所以等待空闲触发更新列表项
      2. 等到滑动到最后一个列表项时,滑不动了,此时空闲,触发更新列表项
      3. 根据当前状态,计算出显示列表项区间[0,11]
      4. render出12个数据项和1个尾部空白区,等待空闲更新列表项
      5. 列表项计算区间没有变化,更新停止,页面状态稳定
    6. 不断向下滑动,找到一个中间状态(比如第一项显示“第62个 title=62”发现有顶部空白和底部空白)


      image
    7. 快速向上滑动,发现会有空白块闪烁一下后再显示出对应内容,滑动流畅


      image
    8. 尝试设置getItemLayout属性,发现不再有数据项的布局回调,而且空白区的高度准确(不会出现滑动到一定位置就滑不动的彩蛋)。这么看好像性能也没有提高多少。
      [图片上传失败...(image-28ff22-1563364857791)]

    9. image

原理

  1. 整个过程就是多render几次,然后就达到平衡态了。
    [https://user-gold-cdn.xitu.io/2019/7/17/16bffc9c0d5ed14b?imageView2/0/w/1280/h/960/format/webp/ignore-error/1]
  2. 整个过程有个窗口的概念,通过一通计算,得到当前状态一个合适的窗口大小。
    [https://user-gold-cdn.xitu.io/2019/7/17/16bffc9cbef91719?imageView2/0/w/1280/h/960/format/webp/ignore-error/1]
  3. 各个文件的依赖关系。整体看一下基本就拼接成现在的功能,核心在VirtualizedList。
    [https://user-gold-cdn.xitu.io/2019/7/17/16bffc9d13784962?imageView2/0/w/1280/h/960/format/webp/ignore-error/1]
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容