Wie kann das Sortieren von HTML-Tabellen beschleunigt werden?

8

Ich bin ein Javascript-Neuling. Nachdem ich viele Javascript- und Jquery-Plugins zum Sortieren meiner HTML-Tabelle ausprobiert hatte und enttäuscht war, entschied ich mich, meinen eigenen Javascript-Code zum Sortieren von HTML-Tabellen zu implementieren. Der Code, den ich geschrieben habe, ist ein Update von 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;
      }
    }
  }
}

Jetzt funktioniert die Sortierung einwandfrei. Es ist jedoch sehr langsam!

Ich beschäftige mich mit vielen Daqta-Zeilen (je nach Projekt können es bis zu 9000 Zeilen sein). Gibt es eine Möglichkeit, meinen Javascript-Code zu beschleunigen?

Lenin Mishra
quelle
3
Entfernen Sie die Zeilen aus dem DOM, sortieren Sie sie und fügen Sie sie erneut dem DOM hinzu ->document.createDocumentFragement()
Andreas
Eigentlich nur die Zeilen verstecken gibt einen sehr göttlichen Effekt. Rendern ist in der Regel die schwere Sache dabei.
Griffin
2
Es ist langsam, weil Sie einen schlechten Sortieralgorithmus verwenden (nach einem kurzen Blick sieht es aus wie Blasensortierung mit Polynomzeit, O(n^2)da es die Tabelle für jede Zeile ( forinnerhalb der while) durchläuft . Verwenden Sie Array.prototype.sortstattdessen den in JavaScript integrierten Sortieralgorithmus bei .
Dai
Wie soll sortFunctionNumericman angerufen werden? Soll nder Spaltenindex sein? (Ich stelle fest, dass Ihre Funktion fehlschlägt, wenn ein colspanoder rowspanin der Tabelle vorhanden ist).
Dai
@Dai Ja. Das nist der Spaltenindex.
Lenin Mishra

Antworten:

6

Es hilft, die Implementierung von Sortieralgorithmen in Browser-JavaScript zu vermeiden, da die in JavaScript integrierte Array.prototype.sortMethode viel schneller ist, selbst wenn Sie am Ende denselben Sortieralgorithmus implementieren (IIRC, die meisten JS-Engines werden wahrscheinlich sowieso QuickSort verwenden ).

So würde ich es machen:

  • Holen Sie sich alle <tr>Elemente in einem JavaScript Array.
    • Sie müssen querySelectorAllin Verbindung mit verwenden, Array.fromweil querySelectorAll kein Array zurückgegeben wird , sondern tatsächlich ein NodeListOf<T>- aber Sie können dies übergeben Array.from, um es in ein zu konvertieren Array.
  • Sobald Sie das haben Array, können Sie Array.prototype.sort(comparison)mit einem benutzerdefinierten Rückruf die Daten aus dem <td>untergeordneten <tr>Element der beiden zu vergleichenden Elemente extrahieren und dann die Daten vergleichen (mit dem x - yTrick beim Vergleichen numerischer Werte. Für stringWerte, die Sie verwenden möchten String.prototype.localeCompare, z return x.localeCompare( y ).
  • Nachdem die Arraysortiert wird (was nicht mehr als ein paar Millisekunden auch nur für einen Tisch mit Zehntausenden von Zeilen, nehmen sollten als QuickSort ist wirklich schnell !) Wieder jede weitere <tr>Verwendung appendChildder Mutter <tbody>.

Meine Implementierung in TypeScript ist unten zusammen mit einem Arbeitsbeispiel mit gültigem JavaScript im Skript-Runner unten aufgeführt:

// 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';
}

Meine sortTableRowsByColumnFunktion setzt Folgendes voraus:

  • Ihr <table>Element verwendet <thead>und hat eine einzige<tbody>
  • Sie verwenden einen modernen Browser , dass Träger =>, Array.from, for( x of y ), :scope, .closest(), und .remove()(also nicht den Internet Explorer 11).
  • Ihre Daten existieren als #text( .textContent) der <td>Elemente.
  • Die Tabelle enthält keine colspanoder rowspanmehrere Zellen.

Hier ist ein lauffähiges Beispiel. Klicken Sie einfach auf die Spaltenüberschriften, um sie in aufsteigender oder absteigender Reihenfolge zu sortieren:

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>

Ein Wort zur Leistung:

Laut dem Performance Analyzer von Developer Tools von Chrome 78 auf meinem Computer zeigen die performance.now()Aufrufe an, dass die Zeilen in etwa 300 ms sortiert wurden. Die Vorgänge "Stil neu berechnen" und "Layout", die ausgeführt werden, nachdem JavaScript nicht mehr ausgeführt wird, dauerten 240 ms bzw. 450 ms ( Die gesamte Relayout-Zeit von 690 ms plus die Sortierzeit von 300 ms bedeutet, dass es eine volle Sekunde (1.000 ms) vom Klicken zum Sortieren gedauert hat.

Wenn ich das Skript so geändert habe, dass die <tr>Elemente zu einem Zwischenprodukt DocumentFragmentanstelle von hinzugefügt werden <tbody>(so dass jeder .appendChildAufruf garantiert keinen Reflow / Layout verursacht, anstatt nur davon auszugehen, dass dies .appendChildkeinen Reflow auslöst) und die Leistung erneut ausgeführt habe Testen Sie mein Ergebnis. Die Timing-Zahlen waren mehr oder weniger identisch (sie waren nach 5 Wiederholungen für eine durchschnittliche Zeit von (1.120 ms) tatsächlich um etwa 120 ms etwas höher - aber ich werde das auf die JIT-Wiedergabe des Browsers zurückführen .

Hier ist der geänderte Code 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 );
    }

Ich denke, die Leistung ist aufgrund des automatischen Tabellenlayout-Algorithmus relativ langsam. Ich wette, wenn ich mein CSS ändere, um table-layout: fixed;die Layoutzeiten zu verwenden, werden sie kürzer. (Update: Ich habe es getestet table-layout: fixed;und überraschenderweise hat das die Leistung überhaupt nicht verbessert - ich kann anscheinend keine besseren Zeiten als 1.000 ms erreichen - na ja).

Dai
quelle
Es besteht keine Notwendigkeit für .remove(). Fügen Sie sie einfach hinzu.
Andreas
@Andreas ah, guter Fang! Ich habe vergessen, dass dies .appendChildein Element bewegen wird.
Dai
Zunächst einmal vielen Dank für Ihre Antwort. Es hilft mir sehr. Muss ich jetzt onclickfür alle Spalten einschließen ? Beispielsweise wird die dritte Spalte nicht sortiert. Also muss ich onclickfür diese Spalte nicht einfügen .. richtig?
Lenin Mishra
@LeninMishra Es gibt viele Möglichkeiten, Event-Handler hinzuzufügen, onclickist einfach die einfachste. Sie können auch .addEventListener('click', onColumnHeaderClicked )innerhalb eines Skripts die Elementobjekte verwenden, die Sie ebenfalls verwenden möchten.
Dai
1
@customcommander Ich habe performance.now()Aufrufe zum Messen hinzugefügt und es sortiert 9000 Zeilen in ca. 300 ms auf meinem Desktop (Chrome 78 x64 auf Core i7 6850K). Ich werde versuchen, Ihren Vorschlag DocumentFragmentjetzt zu verwenden .
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 Zeilen (Zahlen) in 156 ms - 190 ms

Geben Sie hier die Bildbeschreibung ein

Arthur Grigoryan
quelle