前前言
Hello,大家好。 我是来自推啊前端团队的D同学。 今天跟大家简要分享一下虚拟长列表的相关内容。 效果图
1. 设计思路
虚拟列表是按需显示的一种技术,在相似条目非常多的场景下,只渲染可视区域内及周边不多的元素,然后根据用户的滚动来复用Dom元素,属于一种性能优化的手段。
主要的设计思路是:
- 利用可视区之外的Dom元素的复用
- 给容器加上Padding撑开尺寸
- 令可视区之外的区域仅仅是撑开的padding空白,而只需要渲染可视区域和上下几个内容区(为了避免数据过多而产生卡顿,需要提前和滞后渲染相应数据)。
简单Dom结构
<div class="container">
<ul>
<li>2020</li>
<li>2019</li>
<li>2018</li>
<li>2017</li>
<li>2016</li>
<li>2015</li>
<li>2014</li>
</ul>
</div>
接着,在使用JavaScript控制行为之前,需要首先考虑一下临界状态,以及临界状态后的大致行为。
如图
- 在下拉至设定阈值之后,将第一个元素追加到容器内,然后容器加上poddingTop,阈值变化。
- 在上拉至设定阈值之后,把最后一个元素插入到第一个元素之前,减少容器paddingTop,阈值变化。
本次实例中,下拉的初始阈值即是三个lI的高度,上拉的初始阈值为0 。
同时,容器顶部的padding值是由li的变化而动态计算了,因此还要保存一个变量,来计算容器的paddingTop。
2. 主要代码实现
let ul = document.getElementsByTagName('ul')[0];
let item = document.getElementsByTagName('li')[0];
// 单个item的高度
const ItemWidth = parseInt(getComputedStyle(item).height)
let obj = {
initx: 0,
// 控制mousemove的移动与否
isMove: false,
// 初始最大值,用来判断上拉时的界限
max: 0,
// 初始最小值,用来判断下拉时的界限
min: -3 * ItemWidth,
topIndex: 0
}
// 不严谨的获得元素Y轴上的偏移值
function getTransY(el) {
if (!el.style.transform) {
return 0
}
let str = el.style.transform.replace(/.*?,/, '')
return parseInt(str)
}
之后,按部就班的对容器进行鼠标事件监听,实现拖动效果。
ul.addEventListener('mousedown', (e) => {
// 这里记录一下鼠标的坐标,为了计算偏移量
// 记录ul的初始偏移值,是为了加上偏移量,算出总的偏移值
obj.isMove = true,
obj.initY = e.clientY
obj.initTrans = getTransY(ul)
})
ul.addEventListener('mousemove', (e) => {
if(obj.isMove){
// 原有偏移值 + 鼠标滑动距离
let abs = obj.initTrans + (e.clientY - obj.initY)
// 边界判断,不能小于初始状态0
abs = abs > 0 ? 0 : abs
ul.style.transform = `translate3d(0,${abs}px,0)`
if (abs < obj.min) {
obj.max -= ItemWidth
obj.min -= ItemWidth
// changeStatus(0)
console.log('append')
} else if (abs > obj.max) {
obj.max += ItemWidth
obj.min += ItemWidth
// changeStatus(1)
console.log('insertBefore')
}
}
})
ul.addEventListener('mouseup', () => {
obj.isMove = false
})
具体的追加和插入代码先放下,测试一下逻辑上有没有问题。
- 追加
- 插入
可以清晰的看出,在逻辑上是跑得通的,接下来的事情就是对ul内的元素进行操作了。
我们把这个复杂的逻辑提取出来,通过传参来实现追加和插入两种效果。
function changeStatus(flag) {
let lgh = ul.childElementCount
// 获得第一个元素
let fir = ul.children[0]
// 获取最后一个元素
let las = ul.children[lgh - 1]
if (flag) {
// insertBefore
// 这里理论上应该是内容如何变化,这里简单改变了一下值
las.innerText = +fir.innerText + 1
// 这里用于计算paddingTop值的索引减一
obj.topIndex--
// 每插入一个元素,则ul的paddingTop占位值减一个item的高度
ul.style.paddingTop = obj.topIndex * ItemWidth + 'px'
ul.insertBefore(las, fir)
} else {
// append
// 每将第一个元素追加至最后位置,则ul的paddingTop占位值加一个item的高度
obj.topIndex++
ul.style.paddingTop = obj.topIndex * ItemWidth + 'px'
fir.innerText = +las.innerText - 1
ul.appendChild(fir)
}
}
这样处理完完毕,在把ul所在容器的overflow:hidden加上,就可以简单模拟出一个虚拟无限长列表的视觉效果了。
基于该思路,可以简单实现一个虚拟长列表日期选择器。
结语
本文只是简单的介绍一些虚拟长列表的简单实现方式,真是要完成一个比较完整的虚拟长列表的话,还有诸多细节需要去挖掘实现。
不过万变不离其宗,核心即是复用Dom去填充新数据。
比如暴露出自定义配置项,实现纵向还是横向滚动。
或者在非无限滚动的场景下,给定一个拟态滚动条,用来模拟出真实滚动条效果,并且能够让用户大致明白内容的篇幅。
投稿来自「虚拟长列表」