理论与实践:理论上,理论与实践没有区别,但在实践中是有区别的。
- 理论:一切都很清楚,但没有任何作用;
- 练习:一切正常,但没有什么是清楚的;
- 有时理论与实践相遇:没有什么是有效的,也没有什么是清楚的。
有时最好的方法是原型,发现有趣的问题,我花了一点时间来制作一个,尽管作为原型,它确实有很多缺点......
简而言之,限制数据提取积压的最简单解决方案似乎只是在执行提取的例程中设置一个穷人的互斥锁。(在下面的代码示例中,模拟的 fetch 函数是simulateFetchOfData
。)互斥锁涉及在函数范围之外设置一个变量,这样如果false
,则 fetch 可以使用,并且true
当前正在进行 fetch 。
也就是说,当用户调整水平或垂直滑块以开始获取数据时,获取数据的函数首先检查全局变量mutex
是否为真(即,获取已经在进行中),如果是,则简单地退出. 如果mutex
不为真,则设置mutex
为真,然后继续执行提取。当然,在 fetch 函数结束时,mutex
设置为 false,这样下一个用户输入事件将通过前面的互斥体检查,并执行另一个 fetch...
关于原型的几点说明。
- 在该
simulateFetchOfData
函数中,将 sleep(100) 配置为 Promise,用于模拟检索数据的延迟。这夹在控制台上的一些日志记录中。如果您删除互斥检查,您将在控制台打开的情况下看到,在移动滑块时,许多实例simulateFetchOfData
被启动并进入悬念等待睡眠(即模拟获取数据)以解决,而在互斥检查时在适当的位置,任何时候只启动一个实例。
- 可以调整睡眠时间以模拟更大的网络或数据库延迟,以便您可以感受一下用户体验。例如,我正在使用的网络在美国大陆的通信延迟为 90 毫秒。
- 另一个值得注意的是,当完成一次获取并重置
mutex
为 false 后,会执行检查以确定水平和垂直滚动值是否对齐。如果不是,则启动另一个提取。这确保了尽管许多滚动事件可能由于提取繁忙而未触发,但至少通过触发一次最终提取来处理最终滚动值。
- 模拟的单元格数据只是行-破折号-列数的字符串值。例如,“555-333”表示第 555 行第 333 列。
- 一个名为的稀疏数组
buffer
用于保存“获取的”数据。在控制台中检查它会发现许多“empty x XXXX”条目。该simulateFetchOfData
函数设置为如果数据已保存在 中buffer
,则不执行“获取”。
(要查看原型,只需将整个代码复制并粘贴到一个新的文本文件中,重命名为“.html”,然后在浏览器中打开。 编辑: 已在 Chrome 和 Edge 上测试。)
<html><head>
<script>
function initialize() {
window.rowCount = 10000;
window.colCount = 5000;
window.buffer = [];
window.rowHeight = Array( rowCount ).fill( 25 ); // 20px high rows
window.colWidth = Array( colCount ).fill( 70 ); // 70px wide columns
var cellAreaCells = { row: 0, col: 0, height: 0, width: 0 };
window.contentGridCss = [ ...document.styleSheets[ 0 ].rules ].find( rule => rule.selectorText === '.content-grid' );
window.cellArea = document.getElementById( 'cells' );
// Horizontal slider will indicate the left most column.
window.hslider = document.getElementById( 'hslider' );
hslider.min = 0;
hslider.max = colCount;
hslider.oninput = ( event ) => {
updateCells();
}
// Vertical slider will indicate the top most row.
window.vslider = document.getElementById( 'vslider' );
vslider.max = 0;
vslider.min = -rowCount;
vslider.oninput = ( event ) => {
updateCells();
}
function updateCells() {
// Force a recalc of the cell height and width...
simulateFetchOfData( cellArea, cellAreaCells, { row: -parseInt( vslider.value ), col: parseInt( hslider.value ) } );
}
window.mutex = false;
window.lastSkippedRange = null;
window.addEventListener( 'resize', () => {
//cellAreaCells.height = 0;
//cellAreaCells.width = 0;
cellArea.innerHTML = '';
contentGridCss.style[ "grid-template-rows" ] = "0px";
contentGridCss.style[ "grid-template-columns" ] = "0px";
window.initCellAreaSize = { height: document.getElementById( 'cellContainer' ).clientHeight, width: document.getElementById( 'cellContainer' ).clientWidth };
updateCells();
} );
window.dispatchEvent( new Event( 'resize' ) );
}
function sleep( ms ) {
return new Promise(resolve => setTimeout( resolve, ms ));
}
async function simulateFetchOfData( cellArea, curRange, newRange ) {
//
// Global var "mutex" is true if this routine is underway.
// If so, subsequent calls from the sliders will be ignored
// until the current process is complete. Also, if the process
// is underway, capture the last skipped call so that when the
// current finishes, we can ensure that the cells align with the
// settled scroll values.
//
if ( window.mutex ) {
lastSkippedRange = newRange;
return;
}
window.mutex = true;
//
// The cellArea width and height in pixels will tell us how much
// room we have to fill.
//
// row and col is target top/left cell in the cellArea...
//
newRange.height = 0;
let rowPixelTotal = 0;
while ( newRange.row + newRange.height < rowCount && rowPixelTotal < initCellAreaSize.height ) {
rowPixelTotal += rowHeight[ newRange.row + newRange.height ];
newRange.height++;
}
newRange.width = 0;
let colPixelTotal = 0;
while ( newRange.col + newRange.width < colCount && colPixelTotal < initCellAreaSize.width ) {
colPixelTotal += colWidth[ newRange.col + newRange.width ];
newRange.width++;
}
//
// Now the range to acquire is newRange. First, check if this data
// is already available, and if not, fetch the data.
//
function isFilled( buffer, range ) {
for ( let r = range.row; r < range.row + range.height; r++ ) {
for ( let c = range.col; c < range.col + range.width; c++ ) {
if ( buffer[ r ] == null || buffer[ r ][ c ] == null) {
return false;
}
}
}
return true;
}
if ( !isFilled( buffer, newRange ) ) {
// fetch data!
for ( let r = newRange.row; r < newRange.row + newRange.height; r++ ) {
buffer[ r ] = [];
for ( let c = newRange.col; c < newRange.col + newRange.width; c++ ) {
buffer[ r ][ c ] = `${r}-${c} data`;
}
}
console.log( 'Before sleep' );
await sleep(100);
console.log( 'After sleep' );
}
//
// Now that we have the data, let's load it into the cellArea.
//
gridRowSpec = '';
for ( let r = newRange.row; r < newRange.row + newRange.height; r++ ) {
gridRowSpec += rowHeight[ r ] + 'px ';
}
gridColumnSpec = '';
for ( let c = newRange.col; c < newRange.col + newRange.width; c++ ) {
gridColumnSpec += colWidth[ c ] + 'px ';
}
contentGridCss.style[ "grid-template-rows" ] = gridRowSpec;
contentGridCss.style[ "grid-template-columns" ] = gridColumnSpec;
cellArea.innerHTML = '';
for ( let r = newRange.row; r < newRange.row + newRange.height; r++ ) {
for ( let c = newRange.col; c < newRange.col + newRange.width; c++ ) {
let div = document.createElement( 'DIV' );
div.innerText = buffer[ r ][ c ];
cellArea.appendChild( div );
}
}
//
// Let's update the reference to the current range viewed and clear the mutex.
//
curRange = newRange;
window.mutex = false;
//
// One final step. Check to see if the last skipped call to perform an update
// matches with the current scroll bars. If not, let's align the cells with the
// scroll values.
//
if ( lastSkippedRange ) {
if ( !( lastSkippedRange.row === newRange.row && lastSkippedRange.col === newRange.col ) ) {
lastSkippedRange = null;
hslider.dispatchEvent( new Event( 'input' ) );
} else {
lastSkippedRange = null;
}
}
}
</script>
<style>
/*
".range-slider" adapted from... https://codepen.io/ATC-test/pen/myPNqW
See https://www.w3schools.com/howto/howto_js_rangeslider.asp for alternatives.
*/
.range-slider-horizontal {
width: 100%;
height: 20px;
}
.range-slider-vertical {
width: 20px;
height: 100%;
writing-mode: bt-lr; /* IE */
-webkit-appearance: slider-vertical;
}
/* grid container... see https://www.w3schools.com/css/css_grid.asp */
.grid-container {
display: grid;
width: 95%;
height: 95%;
padding: 0px;
grid-gap: 2px;
grid-template-areas:
topLeft column topRight
row cells vslider
botLeft hslider botRight;
grid-template-columns: 50px 95% 27px;
grid-template-rows: 20px 95% 27px;
}
.grid-container > div {
border: 1px solid black;
}
.grid-topLeft {
grid-area: topLeft;
}
.grid-column {
grid-area: column;
}
.grid-topRight {
grid-area: topRight;
}
.grid-row {
grid-area: row;
}
.grid-cells {
grid-area: cells;
}
.grid-vslider {
grid-area: vslider;
}
.grid-botLeft {
grid-area: botLeft;
}
.grid-hslider {
grid-area: hslider;
}
.grid-botRight {
grid-area: botRight;
}
/* Adapted from... https://medium.com/evodeck/responsive-data-tables-with-css-grid-3c58ecf04723 */
.content-grid {
display: grid;
overflow: hidden;
grid-template-rows: 0px; /* Set later by simulateFetchOfData */
grid-template-columns: 0px; /* Set later by simulateFetchOfData */
border-top: 1px solid black;
border-right: 1px solid black;
}
.content-grid > div {
overflow: hidden;
white-space: nowrap;
border-left: 1px solid black;
border-bottom: 1px solid black;
}
</style>
</head><body onload='initialize()'>
<div class='grid-container'>
<div class='topLeft'> TL </div>
<div class='column' id='columns'> column </div>
<div class='topRight'> TR </div>
<div class='row' id = 'rows'> row </div>
<div class='cells' id='cellContainer'>
<div class='content-grid' id='cells'>
Cells...
</div>
</div>
<div class='vslider'> <input id="vslider" type="range" class="range-slider-vertical" step="1" value="0" min="0" max="0"> </div>
<div class='botLeft'> BL </div>
<div class='hslider'> <input id="hslider" type="range" class="range-slider-horizontal" step="1" value="0" min="0" max="0"> </div>
<div class='botRight'> BR </div>
</div>
</body></html>
同样,这是一个原型,用于证明一种限制不必要数据调用积压的方法。如果为了生产目的对其进行重构,许多领域将需要解决,包括:1)减少全局变量空间的使用;2)添加行列标签;3) 向滑块添加按钮以滚动单个行或列;4)如果需要数据计算,可能缓存相关数据;5) 等等...