百万表格难题
之前很火 stackoverflow 上相关问题
大概思路:
不直接显示100W行数据,因为用户可见的数据是有限的(一个可视区不可能同时显示100w行)
所以,我们利用这个有限的可视区高度以及提前设定好的 一个可视区显示多少行数据,来进行优化dom。
假设: 当前用户可视的部分称为 当前页。 *每页显示10条数据 那么总共 10W页
下一部分可视的部分称为 下一页
前一部分可视的部分称为 前一页
用户不可见的部分 我们直接移除DOM,只缓存一定数量的DOM (当前页 以及 前一页 后一页)
用户交互状态:
鼠标滑动浏览
点击上下箭头浏览
直接点击滚动条浏览。
利用js监控以上状态,分别计算比例即可得知用户 浏览那一页。 然后程序显示当前页 并生成前一页后一页
JS部分 (别人的解决方案 main.js
/**
* Created by hebo on 14-1-3.
*/
/*
Visual representation of the approach:
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
==================================================
[=] - real scrollable height (h)
[-] - "pages"; total height of all (n) pages is (th) = (ph) * (n)
The overlap between pages is (cj) and is the distance the scrollbar
will jump when we adjust the scroll position during page switch.
To keep things smooth, we need to minimize both (n) and (cj).
Setting (ph) at 1/100 of (h) is a good start.
*/
var th = 1000000000; // virtual height th = 1,000,000,000 总内容高() 十亿
var h = 1000000; // real scrollable height h = 1,000,000 滚动区域高(#content) 一百万
var ph = h / 100; // page height ph = 1,000,000 / 100 = 10,000 页高 一万
var n = Math.ceil(th / ph); // number of pages n = 1,000,000,000 / 10,000 = 100,000 页数 十万
var vp = 600; // viewport height
var rh = 50; // row height 一页能容纳 10,000 / 50 = 200 行
// 未重叠部分:(h - ph) / (n - 1)
// 重叠部分: ph - (h - ph) / (n - 1) = (ph * n - ph - h + ph) / (n - 1) = (th - h) / (n - 1)
var cj = (th - h) / (n - 1); // "jumpiness" coefficient cj = (1,000,000,000 - 1,000,000) / (100,000 - 1) = 9990.099901 页间重叠区域高度
var page = 0; // current page 当前页码
var offset=0; // current page offset 当前页的位移
var prevScrollTop = 0;
var rows = {}; // cached row nodes
var viewport, content;
$(function() {
viewport = $("#viewport");
content = $("#content");
viewport.css("height",vp);
content.css("height",h);
viewport.scroll(onScroll);
viewport.trigger("scroll");
});
function onScroll() {
var scrollTop = viewport.scrollTop();
// prevScrollTop 在 onNearScroll() 和 onJump() 中会的得到更新
if (Math.abs(scrollTop-prevScrollTop) > vp) {
// 使用鼠标拖动滑块才会触发
console.log('to onJump()');
onJump();
}
else {
// 使用鼠标滚轮或者点击滚动条的箭头按钮会触发
console.log('to onNearScroll()');
onNearScroll();
}
renderViewport();
logDebugInfo();
}
function onNearScroll() {
var scrollTop = viewport.scrollTop();
// console.log('next page: %f, %f', (page + 1)*ph - offset, ph);
console.log('next page: scrollTop: %f, ph: %f, ph + (ph - cj) * page: %f', scrollTop, ph, ph + (ph - cj) * page);
// next page
// 滚动多余一页
if (scrollTop + offset > (page+1)*ph) {
page++;
offset = Math.round(page * cj);
viewport.scrollTop(prevScrollTop = scrollTop - cj);
removeAllRows();
}
// prev page
// 滚动多余一页
else if (scrollTop + offset < page*ph) {
page--;
offset = Math.round(page * cj);
viewport.scrollTop(prevScrollTop = scrollTop + cj);
removeAllRows();
}
else {
prevScrollTop = scrollTop;
}
}
function onJump() {
var scrollTop = viewport.scrollTop();
page = Math.floor(scrollTop * ((th-vp) / (h-vp)) * (1/ph));
offset = Math.round(page * cj);
prevScrollTop = scrollTop;
removeAllRows();
}
function removeAllRows() {
for (var i in rows) {
rows[i].remove();
delete rows[i];
}
}
// 渲染的核心是要取得scrollTop 和 offset
function renderViewport() {
// calculate the viewport + buffer
var y = viewport.scrollTop() + offset,
buffer = vp,
// top 和 bottom 是 一组数据中本组最开头一条的行数,和下一组第一条的行数
// 默认取三屏,一屏的数据数量可以填满一个 viweport(html元素),向前向后各多取一屏
top = Math.floor((y-buffer)/rh),
bottom = Math.ceil((y+vp+buffer)/rh);
top = Math.max(0,top);
bottom = Math.min(th/rh, bottom);
// remove rows no longer in the viewport
for (var i in rows) {
// if (i < top || i > bottom) { // 这里做了修改
if (i < top || i >= bottom) {
rows[i].remove();
delete rows[i];
}
}
// add new rows
// for (var i=top; i<=bottom; i++) { // 这里做了修改,实际上不可能有第th/rh行,因为只有[0, th/rh - 1]
for (var i=top; i<bottom; i++) {
if (!rows[i])
rows[i] = renderRow(i);
}
}
function renderRow(row) {
return $("<div class='row' />")
.css({
top: row*rh - offset,
height: rh
})
.text("row " + (row+1))
.appendTo(content);
}
function logDebugInfo() {
var dbg = $("#debug");
dbg.empty();
dbg.append("n = " + n + "<br>");
dbg.append("ph = " + ph + "<br>");
dbg.append("cj = " + cj + "<br>");
dbg.append("<hr>");
dbg.append("page = " + page + "<br>");
dbg.append("offset = " + offset + "<br>");
dbg.append("virtual y = " + (prevScrollTop + offset) + "<br>");
dbg.append("real y = " + prevScrollTop + "<br>");
dbg.append("rows in the DOM = " + $(".row").length + "<br>");
}
html部分
<!DOCTYPE html>
<html>
<head>
<title>millions of rows</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<script src="jquery-1.4.2.js"></script>
<script src="main.js"></script>
<link rel="stylesheet" href="index.css"/>
</head>
<body>
<div id="viewport">
<div id="content"></div>
</div>
<div id="debug"></div>
<ul>
<li>n -- 页数 </li>
<li>ph -- 页高 </li>
<li>cj -- 页间重叠区域高度 </li>
<li>page -- 当前页码 </li>
<li>offset -- 当前页的位移 </li>
</ul>
</body>
</html>