Vue高性能无限滚动组件的思考与实现

这篇文章阐述了如何一步步实现一个高性能无限滚动组件的实现,最终实现的组件已经收录在awesome-vue仓库中,github代码地址在文章末尾。

笔者目前主要使用Vue技术栈,在工作中碰到一个无限滚动组件(infinite-list)的需求,一个带图带描述信息的列表,如下图所示。

图片列表

初始化时候请求一定数量的一组图片,渲染成列表,等列表滚动到底部时候,再向后端请求下一组图片,更新列表。等列表再滚动到底部,重复上述操作,直到没有更多图片。这是一个非常常见简单的组件,作用是实现列表元素的lazyload,无非就是设定一个高度阈值,监听一下滚动事件,当列表滚动到距离底部不足阈值的时候,加载更多元素,没有更多元素的时候,不再触发加载事件。

我相信,单单实现这个lazyload组件,有一两年开发经验的前端一天就能写出来,那么笔者何必专门写篇文章了?对,问题就是标题的高性能。经验多一些的前端一眼就看出来这个组件的问题,虽然初始的时候列表的元素不多,但随着多次的滚动加载,列表的元素就会越来越多,我们知道,DOM元素越多,页面style和layout的时间越长,所以页面的滚动会越来越卡顿,如果页面是嵌在手机的webview中,某些低端android机器分配给webview的内存不大,甚至会造成app的crash。

那么如何解决这个性能问题了?聪明的读者很快就想出了解决的思路。对,既然问题出在DOM元素过多上,那么我们就想办法来控制DOM元素的个数。对此前端业界有一个通用解决方案叫virtualize,虚拟化,就是把整个列表虚拟化,无论你列表元素有多少,我只虚拟化一定数目的元素(大于一屏幕),然后在滚动过程中动态的更新这些元素,这样的话我们页面重新渲染时候进行的style和layout过程的对象元素就是固定的了,时间不会变长。具体的实现方法参考下图。

列表virtualize(1)

这里假设我们的列表总共有100个元素,每个元素固高为size,设置两个变量remain和bench,分别为5和3。remain就是屏幕上能看到的元素个数,取值为Math.ceil(屏幕高度/size)。bench为缓冲区,屏幕看不到,但是实际存在DOM中。也就是说,实际上我们只渲染总共remain + bench8个元素。那么元素的更新怎么实现了?步骤如下:

1.当列表滚动到item8之前,不做任何操作。

2.滚动到8的时候,此时可见区域的元素是4到8,这时更新DOM的8个元素为4到11,也就是说1-8批量更新成了4-11。但是用户可见区域看到的仍然是4到8,只是bench缓冲区的元素添加了9到11三个元素。

3.元素完成了更新,但是滚动条的位置也要完成更新,因为实际上此时可见区域的item4并不是之前列表DOM中的第4个元素了,而是变成了第1个元素,所以滚动条此时位置变回到了列表的起点,这里需要给列表一个padding-top值,设成3倍元素高度size,从而维持滚动条的位置。

业界大多是按照这个思路来实现virtualize滚动组件的,这里贴一个腾讯alloyteam同事的开源组件,大家先体验一下(10000个元素的滚动)。

大多数场景下,这样设计的组件已经完成了需求,笔者中间也是使用了这个组件,嗯,无论多少元素,也不会造成卡顿和crash了,然而,在快速滚动过程中,发现了另外一个问题,就是列表和滚动条的抖动,并且在列表向上滑动的时候尤其明显,那么原因是什么了?回顾一下上面实现更新的三个步骤。列表往下滚动到8的时候,元素从1-8更新成了4-11,如果这时候你往上滚动了?这个时候item3应当出现,但由于item3已经不在DOM中了,所以又会将整个列表元素更新成3-10,并且更新列表的padding-top来维持滚动条位置。那么继续往上滚动了?2又不在,又得更新成2-9。。。频繁的触发整个列表DOM元素的更新以及重置padding-top的值,导致了抖动的出现。我们在chrome开发者模式下的performance栏记录一下上面贴出来的10000元素滚动的情况(用cpu4xdown来模拟手机),会发现在往上滚动的时候,帧数会明显比往下滚动低,如下图所示。

4xdown的performance

那么还有什么办法能解决这个问题吗?抖动的根本原因是每次更新重置了整个列表DOM的元素,使得每个元素在原本列表文档流中的位置都一同发生了改变,例如4-11更新成3-10,对于item4来说,原本在文档流中排在第一位,更新后变成了第二位,也就是视觉上你会看到item4会突然往下移动了一个item的size,是被item3挤下来的,所以为了维持视觉效果,不得不更新下列表的padding-top值,让item3滚出可视区域,维持item4在可视区域第一位的位置。

所以解决这个问题的根本途径就是将整个列表的元素抽出自上而下的文档流,说一下我的解决步骤:

1. 给每个列表元素加上position:absolute来抽出文档流,并且top都设为0,这样所有的元素都重叠在一起了。

2.给每个元素加上transform:translateY(size * item px),size是元素的高度,item是其本应在列表中的索引值。假设size为20px,也就是item1不动,item2往下位移20px,item3往下位移40px,这样各个元素的位置也就按顺序排列好了。

3.等列表元素需要更新时候,例如4-11更新成3-10,我们只需把item11的位移值改变一下,将其从最底部位置移到第一个位置,并且将11更新成3即可。

4.所有元素都抽出了文档流,所以没有原生滚动条了,那么我们如何滚动列表的位置了,业界有一个通用的滚动解决方案,iScroll,滚动条也是模拟出来的,我们前三个步骤就是基于iScroll来实现的。

利用这个方案改进以后,我们完全消除了抖动,并且在列表元素更新时,无需更新整个列表的元素,只需要更新头部或者尾部的一个元素即可,在快速滚动过程中滚动效果更加平滑。这里贴下我实现的最终组件代码github路径,支持loadmore, pulldown刷新等多种功能,欢迎大家使用,提交issue和改进代码,喜欢地话也请给个star。

GitHub - zuolei828/vue-virtual-infinite-scroll

你可能感兴趣的:(Vue高性能无限滚动组件的思考与实现)