Teoría y práctica: en teoría no hay diferencia entre teoría y práctica, pero en la práctica sí.
- Teoría: todo está claro, pero nada funciona;
- Práctica: todo funciona, pero nada está claro;
- A veces la teoría se encuentra con la práctica: nada funciona y nada está claro.
A veces, el mejor enfoque es un prototipo, y al encontrar el problema interesante, pasé un poco de tiempo cocinando uno, aunque como prototipo tiene muchas verrugas ...
En resumen, la solución más fácil para limitar la acumulación de datos acumulados parece ser simplemente configurar el mutex de un pobre dentro de la rutina que realiza la recuperación. (En el ejemplo de código a continuación, la función de recuperación simulada es simulateFetchOfData
). El mutex implica establecer una variable fuera del alcance de la función, de modo que si false
la recuperación está abierta para su uso y si true
la recuperación está actualmente en curso.
Es decir, cuando el usuario ajusta el control deslizante horizontal o vertical para iniciar una búsqueda de datos, la función que busca los datos primero verifica si la variable global mutex
es verdadera (es decir, una búsqueda ya está en curso), y si es así, simplemente sale . Si mutex
no es verdadero, se establece mutex
en verdadero y luego continúa realizando la búsqueda. Y, por supuesto, al final de la función de recuperación, mutex
se establece en falso, de modo que el próximo evento de entrada del usuario pasará por el control mutex por adelantado y realizará otra recuperación ...
Un par de notas sobre el prototipo.
- Dentro de la
simulateFetchOfData
función, hay suspensión (100) configurada como una Promesa que simula el retraso en la recuperación de los datos. Esto se intercala con algunos registros en la consola. Si elimina la verificación de mutex, verá con la consola abierta que, mientras mueve los controles deslizantes, simulateFetchOfData
se inician muchas instancias y se ponen en suspenso esperando el sueño (es decir, la búsqueda simulada de datos) para resolver, mientras que con la verificación de mutex en su lugar, solo se inicia una instancia a la vez.
- El tiempo de suspensión se puede ajustar para simular una mayor latencia de la red o la base de datos, de modo que pueda tener una idea de la experiencia del usuario. Por ejemplo, las redes en las que tengo experiencia tienen una latencia de 90 ms para las comunicaciones en los Estados Unidos continentales.
- Otro aspecto notable es que al finalizar una búsqueda y después de restablecer
mutex
a falso, se realiza una verificación para determinar si los valores de desplazamiento horizontal y vertical están alineados. Si no, se inicia otra búsqueda. Esto garantiza que, a pesar de que varios eventos de desplazamiento posiblemente no se activen debido a que la búsqueda está ocupada, como mínimo los valores de desplazamiento finales se abordan activando una búsqueda final.
- Los datos de celda simulados son simplemente un valor de cadena de número de fila-guión-columna. Por ejemplo, "555-333" indica la fila 555 columna 333.
- Una matriz dispersa llamada
buffer
se utiliza para contener los datos "recuperados". Examinarlo en la consola revelará muchas entradas "vacías x XXXX". La simulateFetchOfData
función está configurada de tal manera que si los datos ya están almacenados buffer
, no se realiza ninguna "búsqueda".
(Para ver el prototipo, simplemente copie y pegue todo el código en un nuevo archivo de texto, cambie el nombre a ".html" y ábralo en un navegador. EDITAR: Se ha probado en Chrome y 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>
Una vez más, este es un prototipo para demostrar un medio para limitar una acumulación de llamadas de datos innecesarias. Si esto se refactorizara para fines de producción, muchas áreas requerirán abordaje, incluyendo: 1) reducir el uso del espacio variable global; 2) agregar etiquetas de fila y columna; 3) agregar botones a los controles deslizantes para desplazarse por filas o columnas individuales; 4) posiblemente almacenando datos relacionados con el búfer, si se requieren cálculos de datos; 5) etc.