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).
document.createDocumentFragement()