为了解决虚拟列表大量滚动时闪白屏问题,我读懂了「vue-virtual-scroller」源码

2 阅读6分钟

这篇文章值得一写,是有文章介绍了「vue-virtual-scroller」的源码,但代码核心讲的不够,市面上也没找到虚拟列表大量滚动时闪白屏的解决方案。

vue-virtual-scroller 源码传送门

实现效果

废话不说,先看下改动前后效果。

改动前:

修改前.gif

明显可以看出,在隔比较多的 Tab 项切换触发滚动时,出现滚动列表空白现象,而且后面出现的滚动顺序也很奇怪。

改动后:

修改后效果.gif

可能这个 gif 看起来不是特别明显,修改后是出现这个问题时,直接跳转到最后位置,不出现滚动动画。

根本原因

vue-virtual-scroller是通过只渲染可见区域元素来实现视图复用,解决大量数据的情况下的性能瓶颈。那当你超过指定的buffer(默认是 200)后,整个缓存池会进行清理重绘。这在滚动时就出现了断层现象。

源码解析

先说vue-virtual-scroller源码是合理的,它提供的滚动方法scrollToPosition没提供滚动能力。

image.png

源码跳转:RecycleScroller.vue#L732

具体跳转就是红框这句,这句在水平滚动上其实就是设置scrollLeft = {position},这个是直接跳转,没有滚动效果。但我们需要滚动效果,就需要设置它的样式scroll-behavior: smooth;。这就导致出现了闪白屏的现象。如果像官方库那样不要滚动效果,也就不存在这个问题。

重绘逻辑

而要解决这个问题,我们需要先理清它的重绘逻辑,它的重绘逻辑藏的很深,在它最核心的方法里:

image.png

有看过我上一篇我为什么建议前端重视「圈复杂度」?的同学,就能深刻理解读这个圈复杂度 65 的地狱代码是多么痛苦了!

真的吐槽8.9K Star 的三方库代码写成这样,这也导致后面采用的解决办法是把里面算法抽出来一份在外部实现,不然就只能 fork 它这个代码仓库魔改了。

源码跳转:RecycleScroller.vue#L420

这一段代码笔者读了一遍又一遍,虽然这个方法叫做updateVisibleItems(更新可见元素集合),但里面大逻辑包括:查找滚动后的开始索引及结束索引、缓存池更新、视图更新。每一项都有大量的条件边界判断,源码作者还很贴心的写了注释 step 1、step 2 ... 但奈何它的变量命名上一点不注意,各种 index 乱飘,这个二分法算法上命名就更过分了:

image.png

还有它方法前面用到的那一堆变量:

image.png

这需要断个点才能明白这些到底是什么了 ...

好了,重绘逻辑在这里:

image.png

只有触发这个条件,才会重新绘制整个视图。

这些代码是 master 分支的,而笔者用的是 1.1.2 分支的代码:

image.png

这条件判断的根本理解不了 ...

结论:如果不是连续的或整个数据源发生变化了会触发重绘。

这里笔者也是踩了坑,先看懂了 master 的代码,master 代码其实已经是 2.0.0 重构版的代码了,但 1.1.2 分支并不是 ... 但 2.0.0 最后的 tag 也是 beta 版,貌似作者一年前就不再维护了 ...

触发更新

那还有一个问题,谁触发了updateVisibleItems方法?这个源码其实是通过滚动事件监听来做的:

image.png

image.png

这里面还有个 chrome 特殊逻辑,如果是不连续,这玩意儿还会走2遍 ...

如何解决

好了,原因找到了,那如何解决呢?

笔者也是翻了很多资料,看了很多 issues,试了很多方式,但其实能解决问题的方式也不多。

能想到的2种方式:

  1. 一种可能的解决方案是在滚动到目标位置之前,先预加载目标位置的元素。然后在滚动完成后,再加载中间的元素。这样可以确保在滚动过程中不会出现空白。但这基本要深度对vue-virtual-scroller进行源码定制了,而且最后效果上不好说。

  2. 一种是你把大范围的滚动分解成多次小范围的滚动。每次滚动完成后,再进行下一次滚动。这样就可以确保每次滚动都在buffer范围内,这样就都是连续的,不会出现空白。这个方式确实可以不更改源码,只在外部业务上实现,但有个问题,如果滚动区域确实跨度很大,那一次滚动的时间就会相应增加。

按 GPT 的话说:处理大量数据并在此过程中保持良好的用户体验是一项挑战。

这确实很困难,笔者列举几个自己尝试过的方案:

增加 buffer(没意义)

官方其实提供了一种方式:

image.png

修改这个buffer属性,可以增加缓存的最大容量,只要足够大,那理论上都是连续的,不会重绘。

但这有毛用,设置够大,那我用虚拟滚动列表干嘛 ...

去除滚动效果(产品不同意)

按常理,解决不了问题,就解决提出问题的人。去掉滚动效果不就好了。

除了产品不同意以外,也不符合自己的技术底线。

当前效果的方案解析

无论是跳过空白区域还是分段滚动的方案,首先你要解决的问题是业务代码怎么知道现在是不连续的,回到那个地狱,你发现这一长串的判断逻辑,通过各种黑魔法也没办法拿到,且拿到的也不对,因为执行到的时候已经是滚动后了:

image.png

看红框,这里取得是当前的滚动的位置。

而我们的需求是预判断,在滚动前就知道是不连续需要重绘,来进行逻辑处理。

但对笔者公司来说,fork 三方库进行魔改程序上还是很麻烦,且会被质疑。

这规范也合理,毕竟三方库分叉后就很难持续维护。毕竟不是大厂,有众多的大佬可以彻底改造。

那就要有一个不改源码的解决方式:提取它里面判断不连续的方法。(再吐槽下,如果源码作者从那个地狱里封装出来这个方法,就不用这么费事了)

直接放代码:

image.png

image.png

image.png

可以看到,合理的抽离代码后,每个方法的复杂度都不会超过 10,代码清晰非常多。

_getStartIndexByHalfIntervalSearch就是源码中用二分法获取新的startIndex的方法。

然后笔者这里后面实现就比较简单,如果需要重绘就关闭动画,不需要就把动画加回来。

image.png

这样就完成当前方案效果了。

如果产品还要进一步实现更优雅的方案,也可以需要重绘这判断逻辑里进行改造,本文不再展开了。

总结

看源码还是挺有意思的,算是彻底读懂了虚拟列表的实现方式。

还有一点没解释,为什么在白屏后,重绘的列表出现的会这么奇怪。

这是因为它确定位置使用的是绝对定位:

image.png

所以会从头飘过去 ~


感谢阅读,如果对你有用请点个赞 ❤️

中秋节GIF动图引导在看提示.gif