这篇文章值得一写,是有文章介绍了「vue-virtual-scroller」的源码,但代码核心讲的不够,市面上也没找到虚拟列表大量滚动时闪白屏的解决方案。
实现效果
废话不说,先看下改动前后效果。
改动前:
明显可以看出,在隔比较多的 Tab 项切换触发滚动时,出现滚动列表空白现象,而且后面出现的滚动顺序也很奇怪。
改动后:
可能这个 gif 看起来不是特别明显,修改后是出现这个问题时,直接跳转到最后位置,不出现滚动动画。
根本原因
vue-virtual-scroller是通过只渲染可见区域元素来实现视图复用,解决大量数据的情况下的性能瓶颈。那当你超过指定的buffer
(默认是 200)后,整个缓存池会进行清理重绘。这在滚动时就出现了断层现象。
源码解析
先说vue-virtual-scroller源码是合理的,它提供的滚动方法scrollToPosition
没提供滚动能力。
具体跳转就是红框这句,这句在水平滚动上其实就是设置scrollLeft = {position}
,这个是直接跳转,没有滚动效果。但我们需要滚动效果,就需要设置它的样式scroll-behavior: smooth;
。这就导致出现了闪白屏的现象。如果像官方库那样不要滚动效果,也就不存在这个问题。
重绘逻辑
而要解决这个问题,我们需要先理清它的重绘逻辑,它的重绘逻辑藏的很深,在它最核心的方法里:
有看过我上一篇我为什么建议前端重视「圈复杂度」?的同学,就能深刻理解读这个圈复杂度 65 的地狱代码是多么痛苦了!
真的吐槽8.9K Star 的三方库代码写成这样,这也导致后面采用的解决办法是把里面算法抽出来一份在外部实现,不然就只能 fork 它这个代码仓库魔改了。
这一段代码笔者读了一遍又一遍,虽然这个方法叫做updateVisibleItems
(更新可见元素集合),但里面大逻辑包括:查找滚动后的开始索引及结束索引、缓存池更新、视图更新。每一项都有大量的条件边界判断,源码作者还很贴心的写了注释 step 1、step 2 … 但奈何它的变量命名上一点不注意,各种 index 乱飘,这个二分法算法上命名就更过分了:
还有它方法前面用到的那一堆变量:
这需要断个点才能明白这些到底是什么了 …
好了,重绘逻辑在这里:
只有触发这个条件,才会重新绘制整个视图。
这些代码是 master 分支的,而笔者用的是 1.1.2 分支的代码:
这条件判断的根本理解不了 …
结论:如果不是连续的或整个数据源发生变化了会触发重绘。
这里笔者也是踩了坑,先看懂了 master 的代码,master 代码其实已经是 2.0.0 重构版的代码了,但 1.1.2 分支并不是 … 但 2.0.0 最后的 tag 也是 beta 版,貌似作者一年前就不再维护了 …
触发更新
那还有一个问题,谁触发了updateVisibleItems
方法?这个源码其实是通过滚动事件监听来做的:
这里面还有个 chrome 特殊逻辑,如果是不连续,这玩意儿还会走2遍 …
如何解决
好了,原因找到了,那如何解决呢?
笔者也是翻了很多资料,看了很多 issues,试了很多方式,但其实能解决问题的方式也不多。
能想到的2种方式:
-
一种可能的解决方案是在滚动到目标位置之前,先预加载目标位置的元素。然后在滚动完成后,再加载中间的元素。这样可以确保在滚动过程中不会出现空白。但这基本要深度对
vue-virtual-scroller
进行源码定制了,而且最后效果上不好说。 -
一种是你把大范围的滚动分解成多次小范围的滚动。每次滚动完成后,再进行下一次滚动。这样就可以确保每次滚动都在
buffer
范围内,这样就都是连续的,不会出现空白。这个方式确实可以不更改源码,只在外部业务上实现,但有个问题,如果滚动区域确实跨度很大,那一次滚动的时间就会相应增加。
按 GPT 的话说:处理大量数据并在此过程中保持良好的用户体验是一项挑战。
这确实很困难,笔者列举几个自己尝试过的方案:
增加 buffer(没意义)
官方其实提供了一种方式:
修改这个buffer
属性,可以增加缓存的最大容量,只要足够大,那理论上都是连续的,不会重绘。
但这有毛用,设置够大,那我用虚拟滚动列表干嘛 …
去除滚动效果(产品不同意)
按常理,解决不了问题,就解决提出问题的人。去掉滚动效果不就好了。
除了产品不同意以外,也不符合自己的技术底线。
当前效果的方案解析
无论是跳过空白区域还是分段滚动的方案,首先你要解决的问题是业务代码怎么知道现在是不连续的,回到那个地狱,你发现这一长串的判断逻辑,通过各种黑魔法也没办法拿到,且拿到的也不对,因为执行到的时候已经是滚动后了:
看红框,这里取得是当前的滚动的位置。
而我们的需求是预判断,在滚动前就知道是不连续需要重绘,来进行逻辑处理。
但对笔者公司来说,fork 三方库进行魔改程序上还是很麻烦,且会被质疑。
这规范也合理,毕竟三方库分叉后就很难持续维护。毕竟不是大厂,有众多的大佬可以彻底改造。
那就要有一个不改源码的解决方式:提取它里面判断不连续的方法。(再吐槽下,如果源码作者从那个地狱里封装出来这个方法,就不用这么费事了)
直接放代码:
可以看到,合理的抽离代码后,每个方法的复杂度都不会超过 10,代码清晰非常多。
_getStartIndexByHalfIntervalSearch
就是源码中用二分法获取新的startIndex
的方法。
然后笔者这里后面实现就比较简单,如果需要重绘就关闭动画,不需要就把动画加回来。
这样就完成当前方案效果了。
如果产品还要进一步实现更优雅的方案,也可以需要重绘这判断逻辑里进行改造,本文不再展开了。
总结
看源码还是挺有意思的,算是彻底读懂了虚拟列表的实现方式。
还有一点没解释,为什么在白屏后,重绘的列表出现的会这么奇怪。
这是因为它确定位置使用的是绝对定位:
所以会从头飘过去 ~
感谢阅读,如果对你有用请点个赞 ❤️
原文链接:https://juejin.cn/post/7343506152447131658 作者:园宵