Como tornar a classificação de tabelas HTML mais rápida?

8

Eu sou um novato em Javascript. Depois de experimentar muitos plug-ins Javascript e Jquery para classificar minha tabela HTML e acabar decepcionado, decidi implementar meu próprio código Javascript para classificar tabelas HTML. O código que escrevi é uma atualização do 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;
      }
    }
  }
}

Agora a classificação funciona perfeitamente bem. No entanto, é muito lento!

Eu lido com muitas linhas de daqta (dependendo do projeto, ele pode ir até 9000 linhas). Existe uma maneira de acelerar meu código Javascript?

Lenin Mishra
fonte
3
Remova as linhas do DOM, classifique-as e adicione-as novamente ao DOM ->document.createDocumentFragement()
Andreas
Na verdade, apenas esconder as linhas dá um efeito muito divino. Render é geralmente a coisa mais pesada nisso.
Griffin
2
É lento porque você está usando um algoritmo de má classificação (depois de um relance rápida parece que bubble-sort com tempo polinomial O(n^2)porque itera através da tabela para cada linha (o forinterior do while's). Use JavaScript built-in algoritmo de classificação em Array.prototype.sortvez .
Dai
Como você sortFunctionNumericdeve ser invocado? Está ndestinado a ser o índice da coluna? (Observe que sua função falhará se houver uma colspanou rowspanna tabela).
Dai
@Dai Yes. O né o índice da coluna.
Lenin Mishra

Respostas:

6

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).

Dai
fonte
Não há necessidade .remove(). Basta anexá-los.
Andreas
@ Andreas ah, boa captura! Eu esqueci que .appendChildisso moverá um elemento.
Dai
Antes de mais, muito obrigado pela sua resposta. Isso me ajuda muito. Agora, tenho que incluir onclickpara todas as colunas? Por exemplo, a terceira coluna não está sendo classificada. Então eu não tenho que incluir onclicknessa coluna .. certo?
Lenin Mishra
@LeninMishra Existem muitas maneiras de adicionar manipuladores de eventos, onclické apenas a mais simples. Você também pode usar .addEventListener('click', onColumnHeaderClicked )dentro de um script nos objetos de elemento que também deseja usar.
Dai
1
@customcommander Adicionei performance.now()chamadas para medir e classifica por 9000 linhas em cerca de 300ms na minha área de trabalho (Chrome 78 x64 no Core i7 6850K). Vou tentar sua sugestão para usar DocumentFragmentagora.
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 linhas (números) em 156 ms - 190 ms

insira a descrição da imagem aqui

Arthur Grigoryan
fonte