Isso ajuda a evitar a implementação de algoritmos de classificação no JavaScript do navegador, porque o Array.prototype.sortmétodo interno do JavaScript será muito mais rápido, mesmo se você acabar implementando o mesmo algoritmo de classificação (IIRC, a maioria dos mecanismos JS provavelmente usará o QuickSort de qualquer maneira).
Aqui está como eu faria isso:
- Obtenha todos os
<tr>elementos em um JavaScript Array.
- Você precisa usar
querySelectorAllem conjunto com, Array.fromporque querySelectorAll não retorna uma matriz , na verdade, retorna NodeListOf<T>- mas você pode passar isso Array.frompara convertê-lo em um Array.
- Depois de ter o
Array, você pode usar Array.prototype.sort(comparison)com um retorno de chamada personalizado para extrair os dados do <td>filho dos dois <tr>elementos que estão sendo comparados e depois comparar os dados (usando o x - ytruque ao comparar valores numéricos. Para stringvalores que você deseja usar String.prototype.localeCompare, por exemplo, return x.localeCompare( y ).
- Após a
Arrayordenação (que não deve demorar mais do que alguns milissegundos até mesmo para uma tabela com dezenas de milhares de linhas, como o QuickSort é realmente rápido !), Adicione novamente cada <tr>uso appendChilddo pai <tbody>.
Minha implementação no TypeScript está abaixo, juntamente com uma amostra funcional com JavaScript válido no script-runner localizado abaixo:
// 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';
}
Minha sortTableRowsByColumnfunção assume o seguinte:
- Seu
<table>elemento usa <thead>e possui um único<tbody>
- Você está usando um navegador moderno que suporta
=>, Array.from, for( x of y ), :scope, .closest(), e .remove()(ou seja, não o Internet Explorer 11).
- Seus dados existem como os
#text( .textContent) dos <td>elementos.
- Não existam
colspanou rowspancélulas na tabela.
Aqui está uma amostra executável. Basta clicar nos cabeçalhos das colunas para classificar em ordem crescente ou decrescente:
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>
Uma palavra sobre desempenho:
De acordo com o analisador de desempenho das Ferramentas do desenvolvedor do Chrome 78, no meu computador, as performance.now()chamadas indicam que as linhas foram classificadas em cerca de 300ms; no entanto, as operações "Recalcular estilo" e "Layout", que acontecem depois que o JavaScript parou de executar, demoraram 240ms e 450ms, respectivamente ( O tempo total de retransmissão de 690 ms, mais o tempo de classificação de 300 ms, levaram um segundo inteiro (1.000 ms) do clique para a classificação).
Quando mudei o script, de modo que os <tr>elementos sejam adicionados a um intermediário em DocumentFragmentvez de <tbody>(para que cada .appendChildchamada seja garantida para não causar um reflow / layout, em vez de apenas presumir que .appendChildnão causará um reflow) e refiz a performance teste meus números de cronometragem de resultados eram mais ou menos idênticos (na verdade, eram um pouco mais altos em cerca de 120ms no total após 5 repetições, por um tempo médio de (1.120ms) - mas vou colocar isso na reprodução do JIT do navegador .
Aqui está o código alterado 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 );
}
Eu acho que o desempenho é relativamente lento devido ao algoritmo de layout de tabela automático. Aposto que se eu mudar meu CSS para usar table-layout: fixed;o layout, os tempos diminuirão. (Atualização: eu testei table-layout: fixed;e surpreendentemente isso não melhorou o desempenho - parece que não consigo obter tempos melhores que 1.000ms - tudo bem).
document.createDocumentFragement()