虚拟滚动列表的设计
因为 DOM
元素的创建和渲染需要的时间成本很高,在大数据的情况下,完整渲染列表所需要的时间不可接收。其中一个解决思路就是在任何情况下只对「可见区域」进行渲染,可以达到极高的初次渲染性能。
虚拟列表中有两个重要概念
- 可视区域
- 可滚动区域:当列表中的数据总量 * 每一项的高度大于可视区域时,那么就只会展示可视区域中的数据,其他数据通过滚动进行展示。
实现虚拟列表就是处理滚动条滚动后的可见区域的变更,其中具体步骤如下:
- 计算当前可见区域起始数据的
startIndex
- 计算当前可见区域结束数据的
endIndex
- 计算当前可见区域的数据,并渲染到页面中
- 计算
startIndex
对应的数据在整个列表中的偏移位置startOffset
,并设置到列表上
可视区域布局
- 最外层元素(.wrapper)使用相对定位
- 使用一个元素(.list-view)撑起这个列表,让列表的滚动条出现
- 列表的可见元素(.list-view-content)使用绝对定位,left、right、top 设置为 0
<div class="wrapper">
<div class="list-view">
<div class="list-view-content">
<div class="header ">header</div>
<div class="content ">content</div>
<div class="sidebar ">sidebar</div>
<div class="footer ">footer</div>
</div>
</div>
.wrapper{
height:100%;
overflow:auto;
position:relative;
}
.list-view{
height: 100px;
}
.list-view-content{
position: absolute;
top: 0;
left: 0;
right: 0;
}
.header {
height: 50px;
background: saddlebrown;
}
.sidebar{
height: 50px;
background: red;
}
.content{
height: 50px;
background: violet;
}
.footer{
height: 50px;
background: yellow;
}
在这个例子中,.list-view
这个元素的高度就是可视区域的高度,当.list-view-content
中的所有项的高度总和大于可视区域的高度时,就会出现下拉框。
那么,现在的问题就是如何在可视区域中加载对应项的元素了。
计算.list-view
的滚动高度(scrollTop
),除去每一项的高度,就是要可视区域中的要第一个要展示的项的索引。
const shouldStartIndex = Math.floor(scrollTop / nodeHeight);
但是由于索引是从开始算的,所以我们展示的真实节点的索引应该是要减去1的。
const realStartIndex = shouStartIndex - 1;
计算可视区域的最后一项的索引,就是滚动高度(scrollTop
) + 可视高度(clientHeight
) 除于每一项的高度
const shouldEndIndex = Math.ceil((scrollTop + clientHeight) / nodeHeight);
由于有可能已经展示到最后一个了,所以可视区域中最后一个元素要判断一下,因为最后是用slice的方式截取,所以需要加1
const realEndIndex = Math.min(array.length, shouldEndIndex + 1);
最后面就是为可视区域加一个transform
的动画了
clientElement.style.transform = `translateY(${realStartIndex * nodeHeight}px)`;
之后就是slice
截取到对应的数据,重新渲染就可以了。
写在最后
由于elementUI
的tree
组件没有采用虚拟列表的方式,在渲染大数据量时会生成大量的DOM
,因此我使用虚拟滚动树结合ElementUI
的tree
组件的设计思想实现了和 element-ui
API 基本一致的能够应对较大数据量的树组件。
可以通过命令安装后使用
npm i vvtree --save-dev
也可查看源代码:
https://github.com/leopord-lau/VTree