HTMLテーブルのソートを速くする方法は?


8

私はJavascriptの初心者です。多くのJavaScriptおよびJqueryプラグインを試してHTMLテーブルをソートし、がっかりした結果、HTMLテーブルをソートするための独自のJavaScriptコードを実装することにしました。私が書いたコードは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;
      }
    }
  }
}

これで、並べ替えは完全に機能します。しかし、それは非常に遅いです!

daqtaの多くの行を処理します(プロジェクトによっては、9000行まで処理できます)。JavaScriptコードを高速化する方法はありますか?


3
行をDOMから削除し、並べ替えて、DOMに再度追加します->document.createDocumentFragement()
Andreas

実際には行を非表示にするだけで非常に神の影響を与えます。レンダーは通常、この中で重いものです。
グリフィン

2
悪いソートアルゴリズムを使用しているために遅い(一見するとO(n^2)、各行のテーブル(forwhile)を反復するため、多項式時間のバブルソートのように見えます。Array.prototype.sort代わりにJavaScriptの組み込みのソートアルゴリズムを使用してください。

あなたsortFunctionNumericはどのように呼ばれるつもりですか?されたn列のインデックスを意味するもの?(colspanまたはrowspanが表にある場合、関数が失敗することに注意してください)。

@Daiはい。n列インデックスです。
Lenin Mishra

回答:


6

Array.prototype.sort同じソートアルゴリズムを実装しても、JavaScriptの組み込みメソッドの方がはるかに高速になるため、ブラウザーのJavaScriptにソートアルゴリズムを実装しないようにするのに役立ちます(IIRCほとんどのJSエンジンはおそらくQuickSortを使用します)。

ここに私がそれをする方法があります:

  • <tr>JavaScriptのすべての要素を取得しますArray
    • は配列を返さないためquerySelectorAll、と組み合わせて使用する必要があります。これは実際には-を返しますが、これをに渡してに変換できます。Array.fromquerySelectorAll NodeListOf<T>Array.fromArray
  • を取得しArrayたらArray.prototype.sort(comparison)、カスタムコールバックを使用して、比較される<td>2つの<tr>要素の子からデータを抽出し、データを比較できます(x - y数値を比較するときのトリックを使用します。string使用する値にはString.prototype.localeCompare、たとえば、return x.localeCompare( y )
  • Arrayがソートされた後(QuickSortは非常に高速なので、数万行のテーブルでも数ミリ秒以上かかることはありません)、それぞれの親<tr>を使用して再度追加します。appendChild<tbody>

TypeScriptでの私の実装を以下に示します。その下にあるスクリプトランナーに有効なJavaScriptが含まれた実用的なサンプルを示します。

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

私のsortTableRowsByColumn機能は次のことを前提としています。

  • あなたの<table>要素は<thead>単一の<tbody>
  • あなたはサポートされている最新のブラウザを使用している=>Array.fromfor( x of y ):scope.closest()、および.remove()(すなわちないのInternet Explorer 11)。
  • データは要素の#text.textContent)として存在し<td>ます。
  • 何もありませんcolspanまたはrowspanテーブル内のセル。

これは実行可能なサンプルです。列ヘッダーをクリックするだけで、昇順または降順で並べ替えられます。

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>

パフォーマンスについて一言:

Chrome 78のデベロッパーツールのパフォーマンスアナライザーによると、私のコンピューターでは、performance.now()呼び出しは行が約300ミリ秒でソートされたことを示していますが、JavaScriptの実行が停止したに発生する「スタイルの再計算」および「レイアウト」操作は、それぞれ240ミリ秒と450ミリ秒かかりました(合計690msの再レイアウト時間と300msのソート時間は、クリックからソートまでに1秒(1,000ms)かかったことを意味します)。

<tr>要素のDocumentFragment代わりに中間要素に要素が追加されるようにスクリプトを変更したとき<tbody>(各.appendChild呼び出しがリフロー/レイアウトを引き起こさないことが保証されているため、リフロー.appendChildがトリガーされないことを前提としています)、パフォーマンスを再実行しました私の結果のタイミングの数値がほぼ同じであることをテストします(実際には、5回の繰り返しの後、平均時間(1,120ミリ秒)で合計約120ミリ秒わずかに高くなりましたが、それをブラウザーのJITプレイアップに当てはめます。

変更されたコードは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 );
    }

自動テーブルレイアウトアルゴリズムのため、パフォーマンスが比較的遅いと思います。table-layout: fixed;レイアウト時間を使用するようにCSSを変更すると、時間が短縮されます。(更新:私はそれをテストしましたがtable-layout: fixed;、驚くべきことに、パフォーマンスはまったく向上しませんでした。1,000ミリ秒よりも良い時間をとることはできないようです-まあ)。


の必要はありません.remove()。追加するだけです。
Andreas

@Andreasああ、良いキャッチ!.appendChild要素を移動することを忘れました。
Dai

まず第一にあなたの答えに感謝します。それは私を大いに助けます。今、私はonclickすべての列に含める必要がありますか?たとえば、3番目の列はソートされていません。そのonclickため、その列に含める必要はありません。
Lenin Mishra

@LeninMishraイベントハンドラーを追加するには多くの方法がありますが、これonclickは最も単純な方法です。使用し.addEventListener('click', onColumnHeaderClicked )たい要素オブジェクトのスクリプト内でも使用できます。

1
@customcommander私performance.now()は測定するための呼び出しを追加し、それは私のデスクトップ(Core i7 6850K上のChrome 78 x64)で約300msで9000行をソートします。DocumentFragment今すぐあなたの提案を使ってみます。
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>

156ミリ秒-190ミリ秒で9000行(数値)

ここに画像の説明を入力してください

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.