Teoria e prática: Na teoria, não há diferença entre teoria e prática, mas na prática existe.
- Teoria: tudo é claro, mas nada funciona;
- Prática: tudo funciona, mas nada está claro;
- Às vezes, a teoria encontra a prática: nada funciona e nada é claro.
Às vezes, a melhor abordagem é um protótipo e, achando o problema interessante, passei um pouco de tempo preparando um, embora, como protótipo, ele tenha muitas verrugas ...
Em suma, a solução mais fácil para limitar um acúmulo de buscas de dados parece simplesmente configurar o mutex de um homem pobre dentro da rotina que está realizando a busca. (No exemplo de código abaixo, a função de busca simulada é simulateFetchOfData
.) O mutex envolve a configuração de uma variável fora do escopo da função, de modo que false
, se a busca estiver aberta para uso e se true
a busca estiver em andamento no momento.
Ou seja, quando o usuário ajusta o controle deslizante horizontal ou vertical para iniciar uma busca de dados, a função que busca os dados primeiro verifica se a variável global mutex
é verdadeira (ou seja, uma busca já está em andamento) e, nesse caso, simplesmente sai . Se mutex
não for verdadeiro, ele será definido mutex
como true e continuará a busca. E, é claro, no final da função de busca, mutex
é definido como false, de modo que o próximo evento de entrada do usuário passe pela verificação mutex na frente e execute outra busca ...
Algumas notas sobre o protótipo.
- Dentro da
simulateFetchOfData
função, há sleep (100) configurado como uma promessa que simula o atraso na recuperação dos dados. Isso é imprensado com alguns registros no console. Se você remover a verificação mutex, verá com o console aberto que, ao mover os controles deslizantes, muitas instâncias de simulateFetchOfData
são iniciadas e colocadas em suspense aguardando o sono (ou seja, a busca simulada de dados) a ser resolvida, enquanto na verificação mutex no lugar, apenas uma instância é iniciada por vez.
- O tempo de suspensão pode ser ajustado para simular uma maior latência da rede ou do banco de dados, para que você possa ter uma ideia da experiência do usuário. Por exemplo, redes com experiência de latência de 90ms para comunicações nos EUA continentais.
- Outro ponto notável é que, ao concluir uma busca e após redefinir
mutex
para false, é realizada uma verificação para determinar se os valores de rolagem horizontal e vertical estão alinhados. Caso contrário, outra busca é iniciada. Isso garante que, apesar de vários eventos de rolagem possivelmente não serem disparados devido à busca estar ocupada, que no mínimo os valores finais de rolagem sejam endereçados acionando uma busca final.
- Os dados simulados da célula são simplesmente um valor de sequência do número da linha de traço-coluna. Por exemplo, "555-333" indica a linha 555 da coluna 333.
- Uma matriz esparsa denominada
buffer
é usada para armazenar os dados "buscados". Examiná-lo no console revelará muitas entradas "vazias x XXXX". A simulateFetchOfData
função é configurada de modo que, se os dados já estiverem retidos buffer
, nenhuma "busca" seja realizada.
(Para visualizar o protótipo, basta copiar e colar o código inteiro em um novo arquivo de texto, renomeie para ".html" e abra em um navegador. EDIT: Foi testado no Chrome e 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>
Novamente, este é um protótipo para provar um meio de limitar um acúmulo de chamadas de dados desnecessárias. Se isso tiver que ser refatorado para fins de produção, muitas áreas precisarão ser abordadas, incluindo: 1) redução do uso do espaço variável global; 2) adicionando rótulos de linha e coluna; 3) adicionar botões aos controles deslizantes para rolar linhas ou colunas individuais; 4) possivelmente armazenar dados relacionados em buffer, se cálculos de dados forem necessários; 5) etc ...