Làm cách nào để sắp xếp các bảng HTML nhanh hơn?


8

Tôi là một người mới sử dụng Javascript. Sau khi thử nhiều plugin Javascript và Jquery để sắp xếp bảng HTML của tôi và cuối cùng tôi thất vọng, tôi quyết định triển khai mã Javascript của riêng mình để sắp xếp các bảng HTML. Mã tôi đã viết là một bản cập nhật từ 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;
      }
    }
  }
}

Bây giờ việc phân loại hoạt động hoàn toàn tốt. Tuy nhiên, nó rất chậm!

Tôi xử lý rất nhiều hàng daqta (tùy thuộc vào dự án, nó có thể lên tới 9000 hàng). Có cách nào để tăng tốc mã Javascript của tôi không?


3
Xóa các hàng khỏi DOM, sắp xếp chúng, thêm lại chúng vào DOM ->document.createDocumentFragement()
Andreas

Trên thực tế chỉ cần ẩn các hàng cung cấp cho một effekt rất thần. Kết xuất thường là điều nặng nề trong việc này.
Griffin

2
Nó chậm vì bạn đang sử dụng thuật toán sắp xếp kém (sau khi lướt qua nhanh, nó trông giống như sắp xếp bong bóng với thời gian đa thức O(n^2)vì nó lặp qua bảng cho mỗi hàng ( forbên trong while). Array.prototype.sortThay vào đó, hãy sử dụng thuật toán sắp xếp tích hợp của JavaScript .
Đại

Làm thế nào là sortFunctionNumericý nghĩa của bạn để được gọi? Có nnghĩa là chỉ số cột? (Tôi lưu ý rằng chức năng của bạn sẽ thất bại nếu có một colspanhoặc rowspantrong bảng).
Đại

@Dà Có. Đây nlà chỉ số cột.
Lenin Mishra

Câu trả lời:


6

Điều này giúp tránh thực hiện các thuật toán sắp xếp trong JavaScript của trình duyệt vì Array.prototype.sortphương thức tích hợp sẵn của JavaScript sẽ nhanh hơn nhiều ngay cả khi bạn kết thúc thực hiện cùng một thuật toán sắp xếp ( dù sao hầu hết các công cụ JS có thể sẽ sử dụng QuickSort ).

Đây là cách tôi làm:

  • Nhận tất cả các <tr>yếu tố trong một JavaScript Array.
    • Bạn cần sử dụng querySelectorAllkết hợp với Array.fromquerySelectorAll không trả về một mảng , nó thực sự trả về một NodeListOf<T>- nhưng bạn có thể chuyển nó vào Array.fromđể chuyển đổi nó thành một mảngArray .
  • Khi bạn có Array, bạn có thể sử dụng Array.prototype.sort(comparison)với một cuộc gọi lại tùy chỉnh để trích xuất dữ liệu từ phần tử <td>con của hai <tr>phần tử được so sánh, sau đó so sánh dữ liệu (sử dụng x - ymẹo khi so sánh các giá trị số. Đối với stringcác giá trị bạn sẽ muốn sử dụng String.prototype.localeCompare, ví dụ: return x.localeCompare( y ).
  • Sau khi Arrayđược sắp xếp (không mất quá vài mili giây cho ngay cả một bảng có hàng chục nghìn hàng, vì QuickSort thực sự nhanh chóng !) Thêm lại mỗi lần <tr>sử dụng appendChildcủa cha mẹ <tbody>.

Việc triển khai của tôi trong TypeScript nằm bên dưới, cùng với một mẫu đang hoạt động với JavaScript hợp lệ trong trình chạy tập lệnh nằm bên dưới:

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

sortTableRowsByColumnHàm của tôi giả sử như sau:

  • <table>Phần tử của bạn sử dụng <thead>và có một<tbody>
  • Bạn đang sử dụng một trình duyệt hiện đại hỗ trợ =>, Array.from, for( x of y ), :scope, .closest(), và .remove()(tức là không Internet Explorer 11).
  • Dữ liệu của bạn tồn tại dưới dạng #text( .textContent) của các <td>yếu tố.
  • Không có colspanhoặc rowspancác ô trong bảng.

Đây là một mẫu có thể chạy được. Chỉ cần nhấp vào tiêu đề cột để sắp xếp theo thứ tự tăng dần hoặc giảm dần:

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>

Một từ về hiệu suất:

Theo công cụ phân tích Hiệu suất của Công cụ dành cho nhà phát triển của Chrome 78, trên máy tính của tôi, các performance.now()cuộc gọi cho biết các hàng được sắp xếp trong khoảng 300ms, tuy nhiên các thao tác "Tính toán lại kiểu" và "Bố cục" xảy ra sau khi JavaScript ngừng chạy lần lượt mất 240ms và 450ms ( Tổng thời gian chuyển tiếp 690ms, cộng với thời gian sắp xếp 300ms có nghĩa là phải mất toàn bộ giây (1.000ms) từ lần nhấp để sắp xếp).

Khi tôi thay đổi tập lệnh sao cho các <tr>phần tử được thêm vào một trung gian DocumentFragmentthay vì <tbody>(để mỗi .appendChildcuộc gọi được đảm bảo không gây ra phản xạ / bố cục, thay vì chỉ giả sử rằng .appendChildsẽ không kích hoạt phản xạ) và chạy lại hiệu suất kiểm tra các số liệu thời gian kết quả của tôi giống hệt nhau hoặc ít hơn (chúng thực sự cao hơn một chút khoảng 120ms sau 5 lần lặp lại, trong thời gian trung bình là (1.120ms) - nhưng tôi sẽ đưa nó xuống trình phát JIT của trình duyệt .

Đây là mã đã thay đổi bên trong 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 );
    }

Tôi cho rằng hiệu suất tương đối chậm do thuật toán Bố trí bảng tự động. Tôi sẽ đặt cược nếu tôi thay đổi CSS của mình để sử dụng table-layout: fixed;thời gian bố trí sẽ co lại. (Cập nhật: Tôi đã thử nghiệm nó table-layout: fixed;và thật ngạc nhiên là không cải thiện hiệu suất chút nào - tôi dường như không thể có được thời gian tốt hơn 1.000ms - ồ tốt).


Không cần .remove(). Chỉ cần nối chúng.
Andreas

@Andreas ah, bắt tốt! Tôi quên rằng .appendChildsẽ di chuyển một yếu tố.
Đại

Trước hết cảm ơn rất nhiều cho câu trả lời của bạn. Nó giúp tôi rất nhiều. Bây giờ, tôi có phải bao gồm onclicktất cả các cột không? Ví dụ, cột thứ ba không được sắp xếp. Vì vậy, tôi không phải bao gồm onclickcho cột đó .. phải không?
Lenin Mishra

@LeninMishra Có nhiều cách để thêm trình xử lý sự kiện, onclickchỉ đơn giản nhất. Bạn cũng có thể sử dụng .addEventListener('click', onColumnHeaderClicked )bên trong một tập lệnh trên các đối tượng thành phần bạn muốn sử dụng.
Đại

1
@customcommander Tôi đã thêm performance.now()các lệnh gọi để đo và nó sắp xếp qua 9000 hàng trong khoảng 300ms trên máy tính để bàn của tôi (Chrome 78 x64 trên Core i7 6850K). Tôi sẽ thử đề xuất của bạn để sử dụng DocumentFragmentngay bây giờ.
Đại

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 hàng (số) trong 156 ms - 190 ms

nhập mô tả hình ảnh ở đây

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.