相信你们都遇到过渲染一个很长的列表或者页面带来的痛苦,长列表与页面可能对首屏渲染速度形成很大的影响,而且会对页面的滚动形成一些不流畅的体验。html
我也在最近遇到了这个问题,发现除了直接使用分页外,虚拟滚动这种解决方案非常流行,因而也从新造了一下vue中虚拟滚动的轮子。虚拟滚动简单的说就是渲染在浏览器中当前可见的范围内的内容,经过用户滑动滚动条的位置动态地来计算显示内容,其他部分用空白填充来给用户形成一个长列表的假象。前端
这个轮子其实并无你们想象中的复杂,下面就具体介绍一下这个轮子是怎么造的,若是你们在工做中学习中遇到一样的场景也能够适用这种解决方案。如下即是我实现的虚拟滚动的一个简单demo,在线demo与源码。vue
虚拟滚动的核心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-top
与padding-bottom
来控制;再好比直接将列表高度设置成全部元素高度总和,经过定义position
启用top
来进行定位也是能够的......那么有了这个dom结构后咱们就能够来对显示内容进行计算了。数组
首先咱们能够肯定的是列表的可视范围是一段连续的数组内容,因而这个计算就被简化为了找到连续数组内容的开始点与结束点。开始点与结束点依赖的两个信息:一是列表每一项的具体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
有了数组的上界与下界,上下填充高度的计算其实很是直观,咱们以设置列表的padding-top
与padding-bottom
属性为例,如图
不少时候咱们须要优化的不是一个长列表,而是一个长页面,那么对于上述的计算方法有什么改变呢?
首先咱们须要改变可视范围开始点s
与结束点e
的计算方法,对于页面而言可视范围的开始点便是window.pageYOffset || document.documentElement.scrollTop
;结束点是开始点加上可视范围的高度,这里的高度计算咱们使用window.innerHeight || document.documentElement.clientHeight
,但请注意这两个属性在页面有滚动条的时候返回的值是不一样的,innerHeight
会包含滚动条的高度,clientHeight
不包含滚动条的高度。
计算完了可视范围,咱们还须要调整数组y坐标。原先的数组y坐标都是相对于滚动容器而言的,如今咱们须要将数组的y坐标调整为相对于页面。调整方法有两种:一是能够在计算y坐标的时,加上滚动容器的offsetTop
属性;二是能够在计算可视范围开始点s
与结束点e
时,减去滚动容器的offsetTop
属性。
调整完了坐标,咱们还须要将滚动容器的height
与overflow-y
属性去掉,让容器自由生长,同时将滚动容器的scroll
事件转移到window
对象上,这样就实现了对页的虚拟滚动。经过页模式,咱们就能够实现对任何经过固定高度块布局的长页面进行此类的优化。
当滚动刷新数据过于频繁的时候,渲染就会就会产生闪烁,这时咱们就须要经过requestAnimationFrame
来调用更新列表的方法来实现对更新列表速率的控制,从而生成平滑的滚动动画。
vue在这里帮咱们处理了一部分列表更新的问题,好比在滚动形成的小范围数组变更中,vue是会复用先前渲染的节点来进行列表更新的。若是你没有使用相似的框架,那么就须要本身去处理一下这部分的复用逻辑。
除此以外,咱们能够对在必定范围内的渲染内容直接进行缓存,例如咱们能够限定缓存节点数量,在滚动时遇到缓存命中时直接使用缓存中的节点,若是无命中而且缓存节点已满的状况下则可用必定的缓存替换策略,例如用新节点来替换最不频繁使用(LFU)的缓存节点。经过这样的列表缓存来实现对小范围滚动的再次优化。
咱们当前的作法依然是在滚动时对dom进行不停地销毁与再建立,虽然每次建立与销毁dom的开销并不大,可是它们依旧会占用浏览器的一部分性能。
当列表内的每个元素都是经过统一的dom模版或渲染函数进行渲染时,咱们就能够经过列表回收的方式,将超出可视范围的dom节点回收,再将新的数据注入到回收的dom节点中,最后将更新数据后的回收节点放回列表中去,以下图。经过列表回收的方式能够保证你的dom节点总量在一个极低的范围内,而且省去了建立销毁dom这一部分的开销。
最后感谢你的阅读,若是你们有什么意见,建议或者前端相关的问题都欢迎与我交流,这是个人github,have a nice day! :)