¿Cómo hacer que la ordenación de tablas HTML sea más rápida?

8

Soy un novato en Javascript. Después de probar muchos complementos de Javascript y Jquery para ordenar mi tabla HTML y terminar decepcionado, decidí implementar mi propio código Javascript para ordenar las tablas HTML. El código que escribí es una actualización de W3Schools.


function sortFunctionNumeric(n) {
  var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
  table = document.getElementById("reportingTable");
  switching = true;
  //Set the sorting direction to ascending:
  dir = "asc";
  /*Make a loop that will continue until
  no switching has been done:*/
  while (switching) {
    //start by saying: no switching is done:
    switching = false;
    rows = table.rows;
    /*Loop through all table rows (except the
    first, which contains table headers):*/
    for (i = 1; i < (rows.length - 1); i++) {
      //start by saying there should be no switching:
      shouldSwitch = false;
      /*Get the two elements you want to compare,
      one from current row and one from the next:*/
      x = rows[i].getElementsByTagName("TD")[n];
      y = rows[i + 1].getElementsByTagName("TD")[n];
      /*check if the two rows should switch place,
      based on the direction, asc or desc:*/
      if (dir == "asc") {
        if (Number(x.innerHTML) > Number(y.innerHTML)) {
          //if so, mark as a switch and break the loop:
          shouldSwitch = true;
          break;
        }
      } else if (dir == "desc") {
        if (Number(x.innerHTML) < Number(y.innerHTML)) {
          //if so, mark as a switch and break the loop:
          shouldSwitch = true;
          break;
        }
      }
    }
    if (shouldSwitch) {
      /*If a switch has been marked, make the switch
      and mark that a switch has been done:*/
      rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
      switching = true;
      //Each time a switch is done, increase this count by 1:
      switchcount++;
    } else {
      /*If no switching has been done AND the direction is "asc",
      set the direction to "desc" and run the while loop again.*/
      if (switchcount == 0 && dir == "asc") {
        dir = "desc";
        switching = true;
      }
    }
  }
}

Ahora la clasificación funciona perfectamente bien. ¡Sin embargo, es muy lento!

Trato con muchas filas de daqta (dependiendo del proyecto, puede subir hasta 9000 filas). ¿Hay alguna manera de acelerar mi código Javascript?

Lenin Mishra
fuente
3
Elimine las filas del DOM, ordénelas, vuelva a agregarlas al DOM ->document.createDocumentFragement()
Andreas
En realidad, solo ocultar las filas da un efecto muy divino. Renderizar suele ser lo más pesado en esto.
Griffin
2
Es lento porque está utilizando un algoritmo de ordenación incorrecto (después de un vistazo rápido parece una ordenación de burbujas con tiempo polinómico O(n^2)porque itera a través de la tabla para cada fila (el forinterior while). Utilice el algoritmo de ordenación incorporado de JavaScript en su Array.prototype.sortlugar .
Dai
¿Cómo se sortFunctionNumericsupone que debes ser invocado? ¿Está ndestinado a ser el índice de la columna? (Observo que su función fallará si hay un colspano rowspanen la tabla).
Dai
@Dai Sí. El nes el índice de la columna.
Lenin Mishra

Respuestas:

6

Ayuda a evitar la implementación de algoritmos de clasificación en JavaScript del navegador porque el Array.prototype.sortmé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 querySelectorAlljunto con Array.fromporque querySelectorAll no devuelve una matriz , en realidad devuelve un NodeListOf<T>- pero puede pasar esto Array.froma 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 - ytruco al comparar valores numéricos. Para los stringvalores que querrá usar String.prototype.localeCompare, p. Ej. return x.localeCompare( y ).
  • Después de que Arrayse 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 appendChilddel 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 sortTableRowsByColumnfunció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 colspano rowspanceldas 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 DocumentFragmentlugar del <tbody>(para que .appendChildse garantice que cada llamada no cause un reflujo / diseño, en lugar de suponer que .appendChildeso 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).

Dai
fuente
No hay necesidad de eso .remove(). Solo añádelos.
Andreas
@Andreas ah, buena captura! Olvidé que .appendChildmoverá un elemento.
Dai
Antes que nada muchas gracias por tu respuesta. Me ayuda mucho. Ahora, ¿tengo que incluir onclickpara todas las columnas? Por ejemplo, la tercera columna no se está ordenando. Así que no tengo que incluir onclickpara esa columna ... ¿verdad?
Lenin Mishra
@LeninMishra Hay muchas formas de agregar controladores de eventos, onclickes la más simple. También puede usar .addEventListener('click', onColumnHeaderClicked )dentro de un script en los objetos de elementos que desea usar también.
Dai
1
@customcommander Agregué performance.now()llamadas para medir y se clasifica a través de 9000 filas en aproximadamente 300 ms en mi escritorio (Chrome 78 x64 en Core i7 6850K). Probaré tu sugerencia para usar DocumentFragmentahora.
Dai
1

<!DOCTYPE html>
<html>

<head>
    <script>
        function sort_table(tbody, index, sort = (a, b) => {
            if(a < b) return -1; if(a > b) return 1; return 0;}) 
        {
            var list = []
            for (var i = 0; i < tbody.children.length; i++)
                list.push([tbody.children[i].children[index].innerText, tbody.children[i]]);
            list.sort((a, b) => sort(a[0], b[0]));
            var newtbody = document.createElement('tbody');
            for (var i = 0; i < list.length; i++)
                newtbody.appendChild(list[i][1]);
            tbody.parentNode.replaceChild(newtbody, tbody);
            return newtbody;
        }
    </script>
</head>

<body>
    <h2>Unsorted</h2>
    <table>
        <thead>
            <tr>
                <th>Name</th>
                <th>Last Name</th>
                <th>Nationality</th>
                <th>Born</th>
            </tr>
        </thead>
        <tbody>
            <tr><td>Henry</td><td>Cavill</td>
                <td>British</td><td>5 May 1983</td></tr>
            <tr><td>Gal</td><td>Gadot</td>
                <td>Israeli</td><td>30 April 1985</td></tr>
            <tr><td>Olga</td><td>Kurylenko</td>
                <td>Ukrainian</td><td>14 November 1979</td></tr>
            <tr><td>Vincent</td><td>Cassel</td>
                <td>French</td><td>23 November 1966</td></tr>
        </tbody>
    </table>
    <script>
        var table = document.getElementsByTagName('table')[0];
        var named = table.cloneNode(true);
        var dated = table.cloneNode(true);
        document.body.innerHTML += "<h2>Sorted by name</h2>";
        document.body.appendChild(named);

        sort_table(named.children[1], 0); //by name

        document.body.innerHTML += "<h2>Sorted by date</h2>";
        document.body.appendChild(dated);

        sort_table(dated.children[1], 3, (a, b) => { //by date
            if (new Date(a) < new Date(b)) return -1;
            if (new Date(a) > new Date(b)) return 1;
            return 0;
        });
    </script>
</body>

</html>

9000 filas (números) en 156 ms - 190 ms

ingrese la descripción de la imagen aquí

Arthur Grigoryan
fuente