虚拟滚动的轮子是如何形成的?

相信你们都遇到过渲染一个很长的列表或者页面带来的痛苦,长列表与页面可能对首屏渲染速度形成很大的影响,而且会对页面的滚动形成一些不流畅的体验。html

我也在最近遇到了这个问题,发现除了直接使用分页外,虚拟滚动这种解决方案非常流行,因而也从新造了一下vue中虚拟滚动的轮子。虚拟滚动简单的说就是渲染在浏览器中当前可见的范围内的内容,经过用户滑动滚动条的位置动态地来计算显示内容,其他部分用空白填充来给用户形成一个长列表的假象。前端

这个轮子其实并无你们想象中的复杂,下面就具体介绍一下这个轮子是怎么造的,若是你们在工做中学习中遇到一样的场景也能够适用这种解决方案。如下即是我实现的虚拟滚动的一个简单demo,在线demo源码vue

Dom结构

虚拟滚动的核心dom结构其实就是一个简单的列表,在vue中可被描述为以下的代码git

<div style="overflow-y: scroll; height: 300px;" @scroll="handleScroll">
    <div v-for="item in items" :key="`${item.id}`">
        <slot :data="item">
        </slot>
    </div>
</div>
复制代码

这里用了vue的scoped slot来处理用户的自定义dom内容与自定义dom内容的传入数据,若是没有scoped slot,咱们也能够经过让用户在传入数据时,在传入的数据对象中定义一个特定的渲染函数来实现这一步骤。github

有了这个可自定义dom的列表结构后,在外面套一层可滚动的定高的容器,咱们就实现了一个全部列表类组件的基础dom。那么接下来要作的就是填充可视列表之外的滚动高度。这个作法有挺多的,好比在列表上下定义<div>,经过改动<div>高度来控制总高度;好比经过控制列表的padding-toppadding-bottom来控制;再好比直接将列表高度设置成全部元素高度总和,经过定义position启用top来进行定位也是能够的......那么有了这个dom结构后咱们就能够来对显示内容进行计算了。数组

监听滚动事件更新列表内容(handleScroll方法)

1. 计算可视的列表范围

首先咱们能够肯定的是列表的可视范围是一段连续的数组内容,因而这个计算就被简化为了找到连续数组内容的开始点与结束点。开始点与结束点依赖的两个信息:一是列表每一项的具体y坐标,二就是当前可视范围的开始点与结束点。列表每一项的y坐标能够用一次循环经过累加每项的高度来获得每项的y坐标,以下图浏览器

当前可视范围的开始点s便是列表容器的scrollTop属性,而结束点e就是s加上列表容器的高度。如今咱们有了计算数组开始点与结束点的全部信息了,数组的开始点计算就是在全部项的y坐标中寻找到一个不大于可视范围开始点s的项,数组的结束点计算就是在全部项的y坐标中寻找一个不小于e的项,以下图缓存

当数组每一项都为非固定高度的时候,咱们采用二分法(具体实现可参看源码)来寻找数组的上界与下界;当数组每一项为固定高度的时候,咱们能够直接用s除以每项高度向下取整(floor)来获得上界,用e除以每项高度向上取整(ceil)来获得下界。而后用slice方法得到最终须要展现的元素数组。框架

可能你们注意到了,虽然咱们用了二分法O(logN)与直接计算O(1)代替了普通的遍从来寻找上下界,可是slice方法仍是会将总体的复杂度提高到O(N),因此这个优化也仅仅对在有限数组的状况起到必定的提速做用。那么在实际应用中咱们会不会遇到一个至关庞大的数组,大到可以忽视O(logN)O(1)带来的提高呢?答案是否认的,由于浏览器对页面的内存限制咱们很难在实际应用中赶上这样一个数组。dom

2. 计算上下填充高度

有了数组的上界与下界,上下填充高度的计算其实很是直观,咱们以设置列表的padding-toppadding-bottom属性为例,如图

页模式

不少时候咱们须要优化的不是一个长列表,而是一个长页面,那么对于上述的计算方法有什么改变呢?

首先咱们须要改变可视范围开始点s与结束点e的计算方法,对于页面而言可视范围的开始点便是window.pageYOffset || document.documentElement.scrollTop;结束点是开始点加上可视范围的高度,这里的高度计算咱们使用window.innerHeight || document.documentElement.clientHeight,但请注意这两个属性在页面有滚动条的时候返回的值是不一样的,innerHeight会包含滚动条的高度,clientHeight不包含滚动条的高度。

计算完了可视范围,咱们还须要调整数组y坐标。原先的数组y坐标都是相对于滚动容器而言的,如今咱们须要将数组的y坐标调整为相对于页面。调整方法有两种:一是能够在计算y坐标的时,加上滚动容器的offsetTop属性;二是能够在计算可视范围开始点s与结束点e时,减去滚动容器的offsetTop属性。

调整完了坐标,咱们还须要将滚动容器的heightoverflow-y属性去掉,让容器自由生长,同时将滚动容器的scroll事件转移到window对象上,这样就实现了对页的虚拟滚动。经过页模式,咱们就能够实现对任何经过固定高度块布局的长页面进行此类的优化。

更多能够优化的地方

1. 滚动显示优化

当滚动刷新数据过于频繁的时候,渲染就会就会产生闪烁,这时咱们就须要经过requestAnimationFrame来调用更新列表的方法来实现对更新列表速率的控制,从而生成平滑的滚动动画。

2. 列表缓存

vue在这里帮咱们处理了一部分列表更新的问题,好比在滚动形成的小范围数组变更中,vue是会复用先前渲染的节点来进行列表更新的。若是你没有使用相似的框架,那么就须要本身去处理一下这部分的复用逻辑。

除此以外,咱们能够对在必定范围内的渲染内容直接进行缓存,例如咱们能够限定缓存节点数量,在滚动时遇到缓存命中时直接使用缓存中的节点,若是无命中而且缓存节点已满的状况下则可用必定的缓存替换策略,例如用新节点来替换最不频繁使用(LFU)的缓存节点。经过这样的列表缓存来实现对小范围滚动的再次优化。

3. 列表回收

咱们当前的作法依然是在滚动时对dom进行不停地销毁与再建立,虽然每次建立与销毁dom的开销并不大,可是它们依旧会占用浏览器的一部分性能。

当列表内的每个元素都是经过统一的dom模版或渲染函数进行渲染时,咱们就能够经过列表回收的方式,将超出可视范围的dom节点回收,再将新的数据注入到回收的dom节点中,最后将更新数据后的回收节点放回列表中去,以下图。经过列表回收的方式能够保证你的dom节点总量在一个极低的范围内,而且省去了建立销毁dom这一部分的开销。

最后感谢你的阅读,若是你们有什么意见,建议或者前端相关的问题都欢迎与我交流,这是个人github,have a nice day! :)

相关连接

考虑了x轴滚动的vue虚拟滚动组件

加入了列表缓存与列表回收的vue虚拟滚动组件

相关文章
相关标签/搜索