te')); return $arr; } /* 遍历用户所有主题 * @param $uid 用户ID * @param int $page 页数 * @param int $pagesize 每页记录条数 * @param bool $desc 排序方式 TRUE降序 FALSE升序 * @param string $key 返回的数组用那一列的值作为 key * @param array $col 查询哪些列 */ function thread_tid_find_by_uid($uid, $page = 1, $pagesize = 1000, $desc = TRUE, $key = 'tid', $col = array()) { if (empty($uid)) return array(); $orderby = TRUE == $desc ? -1 : 1; $arr = thread_tid__find($cond = array('uid' => $uid), array('tid' => $orderby), $page, $pagesize, $key, $col); return $arr; } // 遍历栏目下tid 支持数组 $fid = array(1,2,3) function thread_tid_find_by_fid($fid, $page = 1, $pagesize = 1000, $desc = TRUE) { if (empty($fid)) return array(); $orderby = TRUE == $desc ? -1 : 1; $arr = thread_tid__find($cond = array('fid' => $fid), array('tid' => $orderby), $page, $pagesize, 'tid', array('tid', 'verify_date')); return $arr; } function thread_tid_delete($tid) { if (empty($tid)) return FALSE; $r = thread_tid__delete(array('tid' => $tid)); return $r; } function thread_tid_count() { $n = thread_tid__count(); return $n; } // 统计用户主题数 大数量下严谨使用非主键统计 function thread_uid_count($uid) { $n = thread_tid__count(array('uid' => $uid)); return $n; } // 统计栏目主题数 大数量下严谨使用非主键统计 function thread_fid_count($fid) { $n = thread_tid__count(array('fid' => $fid)); return $n; } ?>javascript - How to make sorting HTML tables faster? - Stack Overflow
最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to make sorting HTML tables faster? - Stack Overflow

programmeradmin2浏览0评论

I am a Javascript newbie. After trying out many Javascript and Jquery plugins to sort my HTML table and ending up disappointed, I decided to implement my own Javascript code for sorting HTML tables. The code I wrote is an update from 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 pare,
      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;
      }
    }
  }
}

Now the sorting works perfectly fine. However, it is very slow!

I deal with a lot of rows of daqta(depending on project, it can go up to 9000 rows). Is there a way to speed up my Javascript code?

I am a Javascript newbie. After trying out many Javascript and Jquery plugins to sort my HTML table and ending up disappointed, I decided to implement my own Javascript code for sorting HTML tables. The code I wrote is an update from 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 pare,
      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;
      }
    }
  }
}

Now the sorting works perfectly fine. However, it is very slow!

I deal with a lot of rows of daqta(depending on project, it can go up to 9000 rows). Is there a way to speed up my Javascript code?

Share Improve this question edited Dec 11, 2019 at 10:01 cloned 6,7424 gold badges30 silver badges44 bronze badges asked Dec 11, 2019 at 9:44 Lenin MishraLenin Mishra 1812 silver badges16 bronze badges 9
  • 3 Remove the rows from the DOM, sort them, re-add them to the DOM -> document.createDocumentFragement() – Andreas Commented Dec 11, 2019 at 9:48
  • Actually just hide the rows gives a very god effekt. Render is usually the heavy thing in this. – Griffin Commented Dec 11, 2019 at 9:56
  • 2 It's slow because you're using a bad sorting algorithm (after a quick-glance it looks like bubble-sort with polynomial time O(n^2) because it iterates through the table for each row (the for inside the while). Use JavaScript's built-in sorting algorithm at Array.prototype.sort instead. – Dai Commented Dec 11, 2019 at 9:58
  • How is your sortFunctionNumeric meant to be invoked? Is n meant to be the column index? (I note that your function will fail if there is a colspan or rowspan in the table). – Dai Commented Dec 11, 2019 at 10:00
  • @Dai Yes. The n is the column index. – Lenin Mishra Commented Dec 11, 2019 at 10:02
 |  Show 4 more ments

3 Answers 3

Reset to default 12

It helps to avoid implementing sorting algorithms in browser JavaScript because JavaScript's built-in Array.prototype.sort method will be much faster even if you end-up implementing the same sort algorithm (IIRC most JS engines will probably use QuickSort anyway).

Here's how I'd do it:

  • Get all of the <tr> elements in a JavaScript Array.
    • You need to use querySelectorAll in conjunction with Array.from because querySelectorAll does not return an array, it actually returns a NodeListOf<T> - but you can pass this into Array.from to convert it to an Array.
  • Once you have the Array, you can use Array.prototype.sort(parison) with a custom callback to extract the data from the <td> child of the two <tr> elements being pared, and then pare the data (using the x - y trick when paring numeric values. For string values you'll want to use String.prototype.localeCompare, e.g. return x.localeCompare( y ).
  • After the Array is sorted (which should not take more than a few milliseconds for even a table with tens of thousands of rows, as QuickSort is really quick!) re-add each <tr> using appendChild of the parent <tbody>.

My implementation in TypeScript is below, along with a working sample with valid JavaScript in the script-runner located underneath:

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

My sortTableRowsByColumn function assumes the following:

  • Your <table> element uses <thead> and has a single <tbody>
  • You're using a modern browser that supports =>, Array.from, for( x of y ), :scope, .closest(), and .remove() (i.e. not Internet Explorer 11).
  • Your data exists as the #text (.textContent) of the <td> elements.
  • There are no colspan or rowspan cells in the table.

Here's a runnable sample. Simply click the column headers to sort in ascending or descending order:

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>

A word on performance:

According to Chrome 78's Developer Tools' Performance analyzer, on my puter, the performance.now() calls indicate the rows were sorted in about 300ms, however the "Recalculate style" and "Layout" operations which happens after the JavaScript has stopped running took 240ms and 450ms respectively (690ms total relayout time, plus the 300ms sort time means it took a full second (1,000ms) from click-to-sorted).

When I changed the script such that the <tr> elements are added to an intermediate DocumentFragment instead of the <tbody> (so that each .appendChild call is guaranteed to not cause a reflow/layout, instead of just assuming that .appendChild won't trigger a reflow) and re-ran the performance test my result timing figures were more-or-less identical (they were actually slightly higher by about 120ms total after 5 repeats, for an average time of (1,120ms) - but I'll put that down to the browser JIT playing-up.

Here's the changed code inside 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 );
    }

I reckon the performance is relatively slow because of the Automatic Table Layout algorithm. I'll bet if I change my CSS to use table-layout: fixed; the layout times will shrink. (Update: I tested it with table-layout: fixed; and surprisingly that didn't improve performance at all - I can't seem to get better times than 1,000ms - oh well).

<!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 rows (numbers) in 156 ms - 190 ms

I needed to sort a table on a column of addresses: 3 Autumn Lane, 1 Brandon Way, 10 Nature Lane, 100 Pristine Place ... And wanted the sort on street names then house number.

To get that to work, I modified the sortTableRowsByContent function to split numbers from text. Also, since some of my columns have blank values, I hide them when they sort:

function sortTableRowsByColumn( table, columnIndex, ascending ) {

const rows = Array.from( table.querySelectorAll( ':scope > tbody > tr' ) );

rows.sort( ( a, b ) => {

  const aValue = a.cells[columnIndex].textContent;
  const bValue = b.cells[columnIndex].textContent;
  const aNum = parseFloat( aValue );
  const bNum = parseFloat( bValue );

  const aNew = aValue.replace(/^\d{1,3} /, ""); 
  const bNew = bValue.replace(/^\d{1,3} /, "");

  if (ascending){
    return aNew.localeCompare(bNew) || parseInt(aNum) - parseInt(bNum);
  } else {
    return bNew.localeCompare(aNew) || parseInt(bNum) - parseInt(aNum);
  }

} );

for ( i=0; i < rows.length; i++) {
  if (rows[i].getElementsByTagName("td")[columnIndex].textContent == '') {
// this hides the blank rows
    rows[i].style.display = "none";
  } else {
// this restores blanks if a different column is sorted
    rows[i].style.display = "";
  }
};

for( let row of rows ) {
  table.tBodies[0].appendChild( row );
}
}
发布评论

评论列表(0)

  1. 暂无评论