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

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

实现效果

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

改动前:

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

改动后:

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

根本原因

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

源码解析

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

源码跳转:RecycleScroller.vue#L732

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

重绘逻辑

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

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

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

源码跳转:RecycleScroller.vue#L420

这一段代码笔者读了一遍又一遍,虽然这个方法叫做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种方式:

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

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

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

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

增加 buffer(没意义)

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

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

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

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

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

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

当前效果的方案解析

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

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

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

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

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

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

直接放代码:

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

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

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

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

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

总结

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

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

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

所以会从头飘过去 ~


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

相关推荐
一只大侠的侠4 小时前
Flutter开源鸿蒙跨平台训练营 Day 10特惠推荐数据的获取与渲染
flutter·开源·harmonyos
猫头虎8 小时前
如何排查并解决项目启动时报错Error encountered while processing: java.io.IOException: closed 的问题
java·开发语言·jvm·spring boot·python·开源·maven
草梅友仁9 小时前
墨梅博客 1.4.0 发布与开源动态 | 2026 年第 6 周草梅周报
开源·github·ai编程
We་ct12 小时前
LeetCode 56. 合并区间:区间重叠问题的核心解法与代码解析
前端·算法·leetcode·typescript
冬奇Lab13 小时前
一天一个开源项目(第16篇):Code2Video - 用代码生成高质量教学视频的智能框架
开源·aigc·音视频开发
一只大侠的侠13 小时前
Flutter开源鸿蒙跨平台训练营 Day7Flutter+ArkTS双方案实现轮播图+搜索框+导航组件
flutter·开源·harmonyos
聆风吟º13 小时前
CANN开源项目实战指南:使用oam-tools构建自动化故障诊断与运维可观测性体系
运维·开源·自动化·cann
一只大侠的侠14 小时前
Flutter开源鸿蒙跨平台训练营 Day9分类数据的获取与渲染实现
flutter·开源·harmonyos
一只大侠的侠14 小时前
Flutter开源鸿蒙跨平台训练营 Day 5Flutter开发鸿蒙电商应用
flutter·开源·harmonyos