先放一张花瓣网的样式:

浏览器只展示视口内的图片,而其他的图片根本就不在DOM里面,等待用户滚动时,对元素进行动态的增删。
看起来是长列表,其实只展示了一部分,现在有一个词可以用来专门形容这种方式,叫做:
虚拟化列表
今天我们来自己简单的实现一下这个功能,核心思路就是根据滚动位置来算出此时此刻应该显示哪些图片,同时也要具有正常的滚动条,当越往下滚动即内容越来越多时,滚动条也越来越短。
初步代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
ul {
position: relative;
height: 400px;
border: 5px solid red;
overflow-y: scroll;
list-style: none;
padding: 0;
}
li {
box-sizing: border-box;
height: 50px;
border: 2px solid;
padding: 5px;
}
</style>
</head>
<body>
<ul class="list">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
<li>10</li>
<li>11</li>
<li>12</li>
<li>13</li>
<li>14</li>
<li>15</li>
<li>16</li>
<li>17</li>
<li>18</li>
<li>19</li>
<li>20</li>
<li>21</li>
<li>22</li>
<li>23</li>
<li>24</li>
<li>25</li>
<li>26</li>
<li>27</li>
<li>28</li>
<li>29</li>
<li>30</li>
</ul>
<script>
var list = document.querySelector('.list')
var items = new Array(100).fill(0).map((_, i) => i)
var itemHeight = 50
list.addEventListener('scroll', function(e) {
var start = list.scrollTop
var end = start + 400
})
</script>
</body>
</html>

1.

2.

在实际页面中,ul里面一开始应该都是空的,数据都是从服务端获取过来的,假设我们将元素存进items数组当中,第0项则代表第1号元素,第1项代表2号元素,以此类推。
针对第一种情况,要显示2号元素,则
Math.floor(start/50)
得到数字1,以此来代表要从items第1项开始显示,即2号元素
针对第二种情况,要显示11号元素且能继续滚动,则
Math.ceil(end/50) + 1
得到数字12,代表到items第12项之前结束,即选至12号元素就行了
剩下的工作就是根据要在ul中展示哪些项,将它们绝对定位起来
<style>
ul {
position: relative;
height: 400px;
border: 5px solid red;
overflow-y: scroll;
list-style: none;
padding: 0;
}
li {
box-sizing: border-box;
height: 50px;
border: 2px solid;
padding: 5px;
position: absolute;
left: 0;
width: 100%;
}
</style>
<script>
var list = document.querySelector('.list')
var items = new Array(100).fill(0).map((_, i) => i)
var itemHeight = 50
function showContentFromScrollPos(scrollTop) {
var start = scrollTop
var end = start + 400
var startIdx = Math.floor(start / itemHeight)
var endIdx = Math.ceil(end / itemHeight) + 1
list.innerHTML = ''
for(var i = startIdx; i < endIdx; i++) {
var li = document.createElement('li')
li.textContent = items[i+1]
li.style.top = i * itemHeight + 'px'
list.append(li)
}
}
showContentFromScrollPos(0)
list.addEventListener('scroll', function(e) {
var start = list.scrollTop
showContentFromScrollPos(start)
})
</script>

如果只有包了一层ul,是改进不了这个功能的,因为目前的ul是定高的。真的要改进,需要ul外面再包一层进行计算,而ul本身要很长才行,因此需要在停止滚动后,根据情况给ul设置一个高度,这样滚动条的长度就不会因为回滚而变长。接下来我们来改进一下。
<style>
div {
height: 400px;
width: 300px;
border: 5px solid red;
overflow-y: scroll;
}
ul {
position: relative;
width: 300px;
list-style: none;
padding: 0;
margin: 0;
}
li {
box-sizing: border-box;
height: 50px;
border: 2px solid;
padding: 5px;
position: absolute;
left: 0;
width: 100%;
}
</style>
<div class="list-wrap">
<ul class="list" style="height: 0;">
</ul>
</div>
<script>
var list = document.querySelector('.list')
var listWrap = document.querySelector('.list-wrap')
var items = new Array(100).fill(0).map((_, i) => i)
var itemHeight = 50
function showContentFromScrollPos(scrollTop) {
var start = scrollTop
var end = start + 400
var startIdx = Math.floor(start / itemHeight)
var endIdx = Math.min(Math.ceil(end / itemHeight) + 1, items.length)
list.innerHTML = ''
for(var i = startIdx; i < endIdx; i++) {
var li = document.createElement('li')
li.textContent = items[i+1]
li.style.top = i * itemHeight + 'px'
list.append(li)
}
list.style.height = Math.max(i * 50, parseInt(list.style.height)) + 'px'
}
showContentFromScrollPos(0)
listWrap.addEventListener('scroll', function(e) {
var start = listWrap.scrollTop
showContentFromScrollPos(start)
})
</script>
改进效果:

继续改进,现在是定了一共有多少项,接下来模仿从服务器上获取结果,当已有条目数不够时,先获取数据,然后又可以继续滚动。
改进后的代码:
<script>
var list = document.querySelector('.list')
var listWrap = document.querySelector('.list-wrap')
function getItems(start, callback) {
setTimeout(() => {
callback(Array(20).fill(0).map(it => Math.random()))
}, 500)
}
var items = new Array(30).fill(0).map((_, i) => i)
var itemHeight = 50
var loadingItem = false
function showContentFromScrollPos(scrollTop) {
var start = scrollTop
var end = start + 400
var startIdx = Math.floor(start / itemHeight)
var endIdx = Math.ceil(end / itemHeight) + 1
if (endIdx > items.length && loadingItem == false) {
loadingItem = true
console.log('loading')
getItems(items.length, newItems => {
items.push(...newItems)
loadingItem = false
showContentFromScrollPos(scrollTop)
})
return
}
list.innerHTML = ''
for(var i = startIdx; i < endIdx; i++) {
var li = document.createElement('li')
li.textContent = items[i+1]
li.style.top = i * itemHeight + 'px'
list.append(li)
}
list.style.height = Math.max(i * 50, parseInt(list.style.height)) + 'px'
}
showContentFromScrollPos(0)
listWrap.addEventListener('scroll', function(e) {
var start = listWrap.scrollTop
showContentFromScrollPos(start)
})
</script>
最终改进效果:

总的来说,我实现的这个还是有很多瑕疵的,比如说我是滚动的时候直接将视口内所有的li先清空,然后再对它们进行定位,而花瓣网上是删除前面的,再添加后面的,很智能。总之,想完善这种虚拟化列表,还是很复杂的。