发现不足
前端发展了这么多年,已经有了很多优秀的框架、库,很多库都开发了代替原生select标签的组件。这些组件在小数据量时简直不要太好用,然而有时我们将大量数据全部一次性加载到页面上,网页卡死了,除了懒加载,别无他法。
寻找灵感
Android应用里的列表浏览功能跟这个需要几乎一样,我们可以向下滑动浏览上万,上百万条数据。Android是怎么实现的呢?原来Android原生有一个ListAdapter用来重用item。
思路
用头部和尾部的两个div代替被容器挡住的item,监听容器滚动条的位置变化,动态设置两个div的高度,同时items中的内容从js数据取对应位置的内容填充。
代码
CSS
.combo,
.combo * {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.combo {
position: relative;
width: 200px;
height: 260px;
border: 1px solid #666666;
background: #FFFFFF;
}
.combo .combo-container {
height: 100%;
position: relative;
overflow: auto;
}
.combo ul {
margin: 0;
padding: 0;
}
.combo li {
height: 32px;
list-style: none;
margin: 0;
padding: 2px;
}
.combo li span {
height: 100%;
font-family: sans-serif;
white-space: nowrap;
display: inline-block;
font-size: 16px;
line-height: 30px;
cursor: pointer;
}
.combo li span.even {
background-color: #DDDDDD;
}
HTML
<div id="combo" class="combo">
<div id="combo-container" class="combo-container">
<div id="combo-before1"></div>
<div id="combo-before2"></div>
<div id="combo-before3"></div>
<div id="combo-before4"></div>
<ul>
</ul>
<div id="combo-after1"></div>
<div id="combo-after2"></div>
<div id="combo-after3"></div>
<div id="combo-after4"></div>
</div>
</div>
Javascript
window.onload = function () {
var perLiHeight = 32;
var arr = [];
for (var i = 0; i < 190000; i++) {
arr.push(i + 'abc defg hijkl mno pq rst '+ (i % 80 > 77 ? '54345 6547 67654' : ''));
}
var comboContainer = document.getElementById('combo-container');
var comboBefore1 = document.getElementById('combo-before1');
var comboBefore2 = document.getElementById('combo-before2');
var comboBefore3 = document.getElementById('combo-before3');
var comboBefore4 = document.getElementById('combo-before4');
var comboAfter1 = document.getElementById('combo-after1');
var comboAfter2 = document.getElementById('combo-after2');
var comboAfter3 = document.getElementById('combo-after3');
var comboAfter4 = document.getElementById('combo-after4');
var ul = comboContainer.getElementsByTagName('ul')[0];
var comboHeight = comboContainer.clientHeight;
var liMaxCount = Math.max(Math.floor(comboHeight / perLiHeight) + 2, 80);
var lisHeight = liMaxCount * perLiHeight;
var realHeight = arr.length * perLiHeight;
var realCount = Math.min(arr.length, liMaxCount);
var li = document.createElement('li');
var span = document.createElement('span');
for (var i = 0; i < realCount; i++) {
var node = li.cloneNode();
var sp = span.cloneNode();
sp.innerText = arr[i];
node.appendChild(sp);
if (i % 2 === 0) {
sp.className = 'even';
}
ul.appendChild(node);
}
ul.onclick = function (e) {
var target = e ? e.target : window.event.srcElement;
alert(target.innerText);
};
comboBefore1.style.height = 0 + 'px';
comboBefore2.style.height = 0 + 'px';
comboBefore3.style.height = 0 + 'px';
comboBefore4.style.height = 0 + 'px';
comboAfter1.style.height = Math.max(Math.floor((realHeight - lisHeight) / 4), 0) + 'px';
comboAfter2.style.height = Math.max(Math.floor((realHeight - lisHeight) / 4), 0) + 'px';
comboAfter3.style.height = Math.max(Math.floor((realHeight - lisHeight) / 4), 0) + 'px';
comboAfter4.style.height = Math.max(Math.floor((realHeight - lisHeight) / 4), 0) + 'px';
var lis = ul.getElementsByTagName('li');
comboContainer.onscroll = function () {
var scrollTop = comboContainer.scrollTop;
var index = Math.floor(scrollTop / perLiHeight);
var top = index * perLiHeight;
var comboBeforeFloorHeight = Math.max(Math.min(Math.floor(top / 4), Math.floor((realHeight - lisHeight) / 4)), 0);
var comboBeforeCeilHeight = Math.max(Math.min(Math.ceil(top / 4), Math.ceil((realHeight - lisHeight) / 4)), 0);
var comboAfterFloorHeight = Math.max(Math.floor((realHeight - lisHeight - top) / 4), 0);
var comboAfterCeilHeight = Math.max(Math.ceil((realHeight - lisHeight - top) / 4), 0);
comboBefore1.style.height = comboBeforeFloorHeight + 'px';
comboBefore2.style.height = comboBeforeFloorHeight + 'px';
comboBefore3.style.height = comboBeforeCeilHeight + 'px';
comboBefore4.style.height = comboBeforeCeilHeight + 'px';
comboAfter1.style.height = comboAfterFloorHeight + 'px';
comboAfter2.style.height = comboAfterFloorHeight + 'px';
comboAfter3.style.height = comboAfterCeilHeight + 'px';
comboAfter4.style.height = comboAfterCeilHeight + 'px';
comboContainer.scrollTop = scrollTop;
var sp = null, i = 0;
if (realCount + index < arr.length) {
for (i = 0; i < realCount; i++) {
sp = lis[i].getElementsByTagName('span')[0];
sp.innerText = arr[i + index];
if ((i + index) % 2 === 0) {
sp.className = 'even';
} else {
sp.className = '';
}
}
} else {
for (i = 0; i < realCount; i++) {
sp = lis[i].getElementsByTagName('span')[0];
sp.innerText = arr[i + arr.length - realCount];
if ((i + arr.length - realCount) % 2 === 0) {
sp.className = 'even';
} else {
sp.className = '';
}
}
}
}
}
说明
为什么要用4个头部,4个尾部, 而不是用一个呢?因为浏览器对元素的高度,尤其动态设置高度是有限制的。微软系浏览器性能就呵呵了。item高度越大,浏览器能正常显示的数据就越少。如果只是一个头部一个尾部,且item高度为32px时,微软系浏览器只能正常显示50000条数据。面4个头部4个尾部,微软系浏览器能正常显示190000条数据。
为什么不用8个头部尾部或16个头部尾部呢?很不幸,经测试头部尾部大于4个时,浏览器能正常显示的item并没有增加。不知道以后会不会找到更好的办法。