Ayuda a evitar la implementación de algoritmos de clasificación en JavaScript del navegador porque el Array.prototype.sort
método incorporado de JavaScript será mucho más rápido incluso si termina implementando el mismo algoritmo de clasificación (IIRC la mayoría de los motores JS probablemente usarán QuickSort de todos modos).
Así es como lo haría:
- Obtenga todos los
<tr>
elementos en un JavaScript Array
.
- Debe usar
querySelectorAll
junto con Array.from
porque querySelectorAll
no devuelve una matriz , en realidad devuelve un NodeListOf<T>
- pero puede pasar esto Array.from
a convertirlo en un Array
.
- Una vez que tenga el
Array
, puede usarlo Array.prototype.sort(comparison)
con una devolución de llamada personalizada para extraer los datos del elemento <td>
secundario de los dos <tr>
elementos que se están comparando, y luego comparar los datos (usando el x - y
truco al comparar valores numéricos. Para los string
valores que querrá usar String.prototype.localeCompare
, p. Ej. return x.localeCompare( y )
.
- Después de que
Array
se ordena (lo que no debería tomar más de unos pocos milisegundos incluso para una tabla con decenas de miles de filas, ¡ ya que QuickSort es realmente rápido !) Vuelva a agregar cada <tr>
uso appendChild
del padre <tbody>
.
Mi implementación en TypeScript está a continuación, junto con una muestra de trabajo con JavaScript válido en el script-runner ubicado debajo:
// This code has TypeScript type annotations, but can be used directly as pure JavaScript by just removing the type annotations first.
function sortTableRowsByColumn( table: HTMLTableElement, columnIndex: number, ascending: boolean ): void {
const rows = Array.from( table.querySelectorAll( ':scope > tbody > tr' ) );
rows.sort( ( x: HTMLtableRowElement, y: HTMLtableRowElement ) => {
const xValue: string = x.cells[columnIndex].textContent;
const yValue: string = y.cells[columnIndex].textContent;
// Assuming values are numeric (use parseInt or parseFloat):
const xNum = parseFloat( xValue );
const yNum = parseFloat( yValue );
return ascending ? ( xNum - yNum ) : ( yNum - xNum ); // <-- Neat comparison trick.
} );
// There is no need to remove the rows prior to adding them in-order because `.appendChild` will relocate existing nodes.
for( let row of rows ) {
table.tBodies[0].appendChild( row );
}
}
function onColumnHeaderClicked( ev: Event ): void {
const th = ev.currentTarget as HTMLTableCellElement;
const table = th.closest( 'table' );
const thIndex: number = Array.from( th.parentElement.children ).indexOf( th );
const ascending = ( th.dataset as any ).sort != 'asc';
sortTableRowsByColumn( table, thIndex, ascending );
const allTh = table.querySelectorAll( ':scope > thead > tr > th' );
for( let th2 of allTh ) {
delete th2.dataset['sort'];
}
th.dataset['sort'] = ascending ? 'asc' : 'desc';
}
Mi sortTableRowsByColumn
función asume lo siguiente:
- Su
<table>
elemento usa <thead>
y tiene un solo<tbody>
- Estás usando un navegador moderno que los apoyos
=>
, Array.from
, for( x of y )
, :scope
, .closest()
, y .remove()
(es decir, no Internet Explorer 11).
- Sus datos existen como
#text
( .textContent
) de los <td>
elementos.
- No hay
colspan
o rowspan
celdas en la tabla.
Aquí hay una muestra ejecutable. Simplemente haga clic en los encabezados de columna para ordenar en orden ascendente o descendente:
function sortTableRowsByColumn( table, columnIndex, ascending ) {
const rows = Array.from( table.querySelectorAll( ':scope > tbody > tr' ) );
rows.sort( ( x, y ) => {
const xValue = x.cells[columnIndex].textContent;
const yValue = y.cells[columnIndex].textContent;
const xNum = parseFloat( xValue );
const yNum = parseFloat( yValue );
return ascending ? ( xNum - yNum ) : ( yNum - xNum );
} );
for( let row of rows ) {
table.tBodies[0].appendChild( row );
}
}
function onColumnHeaderClicked( ev ) {
const th = ev.currentTarget;
const table = th.closest( 'table' );
const thIndex = Array.from( th.parentElement.children ).indexOf( th );
const ascending = !( 'sort' in th.dataset ) || th.dataset.sort != 'asc';
const start = performance.now();
sortTableRowsByColumn( table, thIndex, ascending );
const end = performance.now();
console.log( "Sorted table rows in %d ms.", end - start );
const allTh = table.querySelectorAll( ':scope > thead > tr > th' );
for( let th2 of allTh ) {
delete th2.dataset['sort'];
}
th.dataset['sort'] = ascending ? 'asc' : 'desc';
}
window.addEventListener( 'DOMContentLoaded', function() {
const table = document.querySelector( 'table' );
const tb = table.tBodies[0];
const start = performance.now();
for( let i = 0; i < 9000; i++ ) {
let row = table.insertRow( -1 );
row.insertCell( -1 ).textContent = Math.ceil( Math.random() * 1000 );
row.insertCell( -1 ).textContent = Math.ceil( Math.random() * 1000 );
row.insertCell( -1 ).textContent = Math.ceil( Math.random() * 1000 );
}
const end = performance.now();
console.log( "IT'S OVER 9000 ROWS added in %d ms.", end - start );
} );
html { font-family: sans-serif; }
table {
border-collapse: collapse;
border: 1px solid #ccc;
}
table > thead > tr > th {
cursor: pointer;
}
table > thead > tr > th[data-sort=asc] {
background-color: blue;
color: white;
}
table > thead > tr > th[data-sort=desc] {
background-color: red;
color: white;
}
table th,
table td {
border: 1px solid #bbb;
padding: 0.25em 0.5em;
}
<table>
<thead>
<tr>
<th onclick="onColumnHeaderClicked(event)">Foo</th>
<th onclick="onColumnHeaderClicked(event)">Bar</th>
<th onclick="onColumnHeaderClicked(event)">Baz</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>9</td>
<td>a</td>
</tr>
<!-- 9,000 additional rows will be added by the DOMContentLoaded event-handler when this snippet is executed. -->
</tbody>
</table>
Una palabra sobre el rendimiento:
De acuerdo con el analizador de rendimiento de Developer Tools de Chrome 78, en mi computadora, las performance.now()
llamadas indican que las filas se ordenaron en aproximadamente 300 ms, sin embargo, las operaciones de "Recalcular estilo" y "Diseño" que ocurren después de que JavaScript dejó de funcionar tomaron 240 ms y 450 ms respectivamente ( El tiempo total de retransmisión de 690 ms, más el tiempo de clasificación de 300 ms significa que tomó un segundo completo (1,000 ms) desde hacer clic para ordenar).
Cuando cambié el script de tal manera que los <tr>
elementos se agregan a un intermedio en DocumentFragment
lugar del <tbody>
(para que .appendChild
se garantice que cada llamada no cause un reflujo / diseño, en lugar de suponer que .appendChild
eso no desencadenará un reflujo) y volvió a ejecutar el rendimiento probar las cifras de tiempo de resultados fueron más o menos idénticas (en realidad fueron un poco más altas en aproximadamente 120 ms en total después de 5 repeticiones, durante un tiempo promedio de (1.120 ms), pero lo atribuiré a la reproducción JIT del navegador .
Aquí está el código cambiado dentro sortTableRowsByColumn
:
function sortTableRowsByColumn( table, columnIndex, ascending ) {
const rows = Array.from( table.querySelectorAll( ':scope > tbody > tr' ) );
rows.sort( ( x, y ) => {
const xValue = x.cells[columnIndex].textContent;
const yValue = y.cells[columnIndex].textContent;
const xNum = parseFloat( xValue );
const yNum = parseFloat( yValue );
return ascending ? ( xNum - yNum ) : ( yNum - xNum );
} );
const fragment = new DocumentFragment();
for( let row of rows ) {
fragment.appendChild( row );
}
table.tBodies[0].appendChild( fragment );
}
Creo que el rendimiento es relativamente lento debido al algoritmo de diseño automático de tabla. Apuesto a que si cambio mi CSS para usar, table-layout: fixed;
los tiempos de diseño se reducirán. (Actualización: lo probé table-layout: fixed;
y, sorprendentemente, eso no mejoró el rendimiento en absoluto; parece que no puedo obtener mejores tiempos que 1,000 ms, oh, bueno).
document.createDocumentFragement()
O(n^2)
porque itera a través de la tabla para cada fila (elfor
interiorwhile
). Utilice el algoritmo de ordenación incorporado de JavaScript en suArray.prototype.sort
lugar .sortFunctionNumeric
supone que debes ser invocado? ¿Están
destinado a ser el índice de la columna? (Observo que su función fallará si hay uncolspan
orowspan
en la tabla).n
es el índice de la columna.