首先,需要有一个滚动容器 div 来放置滚动列表 ul;按照通常的渲染方式,ul 里会有大量(成千上万)的列表元素 li;当对某一个 li 进行 DOM 操作时(比如,一些股票软件会通过高亮的方式实时渲染此支股票的涨跌),需要先遍历所有 li,找到要进行操作的元素,再进行 DOM 操作。此时,页面可能会因为 DOM 树过于庞大而占用大量内存,页面渲染可能卡顿。而虚拟滚动要做的,就是在保证用户正常交互体验的同时尽可能少的渲染 DOM,提升页面的响应速度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
<style>
* {
margin: 0;
padding: 0;
}
body {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
height: 80vh;
width: 80vw;
overflow-y: scroll;
outline: 1px solid orange;
}
.list {
box-sizing: border-box;
}
.list li {
width: 100%;
height: 36px;
outline: 1px solid red;
text-align: center;
line-height: 36px;
}
</style>
<div class="container">
<ul class="list">
<!-- <li>...</li> -->
<!-- <li>...</li> -->
<!-- <li>...</li> -->
<!-- <li>...</li> -->
<!-- <li>...</li> -->
<!-- ... -->
</ul>
</div>
|
具体逻辑如下:
- 首先计算用户可以看到的元素数量;
- 根据滚动条高度,计算从那个数据开始渲染;
- 滚动时需要动态计算需要填充的 padding 和数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
const listData = Array.from({ length: 3600 }, (v, i) => `第 ${i + 1} 条数据`); // 假数据
const containerEle = document.querySelector('.container'); // 滚动容器
const listEle = document.querySelector('.list'); // 滚动列表
class VirtualScroll {
/**
* containerEle 滚动容器
* listEle 滚动列表
* listData 列表数据
* itemHeight 元素高度
*/
constructor(containerEle, listEle, listData, itemHeight = 36) {
this.containerEle = containerEle;
this.listEle = listEle;
this.listData = listData;
this.itemHeight = itemHeight;
this.viewMax = 0; // 可视元素个数
this.renderIndex = 0; // 渲染起始位置
this.renderData = []; // 渲染数据
this.init();
}
// 初始化
init() {
this.containerEle.scrollTop = 0;
this.viewMax =
Math.floor(this.containerEle.clientHeight / this.itemHeight) + 1;
this.fillRenderData();
this.renderDOM();
this.modifyListElePadding();
this.containerEle.removeEventListener('scroll', this.handleScroll);
this.containerEle.addEventListener('scroll', this.handleScroll);
}
// 填充渲染数据
fillRenderData() {
// 仅需要渲染比 this.viewMax 稍多的元素即可(这里取了两倍),不然滚动起来会露馅儿
this.renderData = this.listData.slice(
this.renderIndex,
this.renderIndex + this.viewMax * 2
);
}
// 渲染 DOM
renderDOM() {
const fragment = document.createDocumentFragment();
this.renderData.forEach(item => {
const liEle = document.createElement('li');
liEle.textContent = item;
fragment.appendChild(liEle);
});
this.listEle.innerHTML = ''; // 渲染前先清空旧的
this.listEle.appendChild(fragment);
}
// 修改填充高度
modifyListElePadding() {
// ul 里除了 li,还需要设置 padding(top/bottom)将 ul 高度撑起来,否则滚动条会露馅儿
const listElePadding =
this.itemHeight * (this.listData.length - this.renderData.length); // 总 padding(top + bottom)
const listElePaddingTop = this.itemHeight * this.renderIndex;
const listElePaddingBottom = listElePadding - listElePaddingTop;
this.listEle.style.padding = `${listElePaddingTop}px 0 ${listElePaddingBottom}px`;
}
// 处理滚动行为
handleScroll = e => {
const scrollTop = e.currentTarget.scrollTop || 0; // 获取滚动高度
this.renderIndex = Math.floor(scrollTop / this.itemHeight); // 根据滚动高度设置渲染起始位置
// 重新进行 DOM 填充即可
this.fillRenderData();
this.renderDOM();
this.modifyListElePadding();
};
}
new VirtualScroll(containerEle, listEle, listData);
|
DEMO 在
这里
。