簡體   English   中英

如何使用純 Javascript 過濾非常大的引導表

[英]How to filter a very large bootstrap table using pure Javascript

我在 bootstrap 中構建了一個大表,大約 5,000 行 x 10 列,我需要快速過濾特定屬性的表,並且只使用 JavaScript。 該表有一個 id 列和一個屬性列,即

id | attr | ...
---------------
2  |  X   | ...
3  |  Y   | ...
4  |  X   | ...

為了加快過濾過程,我構建了一個哈希表,將屬性映射回列 ID。 例如,我有一個映射:

getRowIds["X"] = [2,4]

用戶可以在搜索框中輸入屬性“X”,哈希表然后查找包含“X”的相應行(在本例中為 2 和 4),然后通過映射操作調用以下函數:

this.hideRow = function(id) {
    document.getElementById(id).style.display="none"
}

this.showRow = function(id) {
    document.getElementById(id).style.display=""
}

這個過程仍然很慢,因為用戶可以選擇多個屬性(比如 X、Y)。

有沒有更快的隱藏行的方法?

如果我能以某種方式將表與 DOM 分離,進行更改,然后重新附加,會更快嗎? 我如何在 javascript 中做到這一點?

是否有其他更有效/更智能的過濾方法?

謝謝 :)

我會問

  • 為什么要為自己編寫此代碼? 根據個人經驗,嘗試在所有瀏覽器上進行有效過濾是一項不平凡的任務。
  • 如果您將此作為學習經驗,請查看下面列出的軟件包的來源作為示例。
  • 對於 5000 行,進行服務器端過濾和排序會更有效。 然后使用ajax更新顯示的表格。

我建議您考慮使用已經這樣做的幾個 JavaScript 包之一。 下面兩個包還有更多的包。 我將這兩個展示為可用內容的示例。

您最好的選擇是不渲染所有這些內容並存儲它們的對象版本,並且通過分頁一次最多顯示 50 行。 在內存中存儲那么多對象,在 JS 中是沒有問題的。 另一方面,將所有這些存儲在 DOM 中將使瀏覽器屈服。 5000 大約是瀏覽器在一台好機器上可以做的同時保持良好性能的上限。 如果您開始修改其中一些行並調整內容(“隱藏”、“顯示”),事情肯定會變得更慢。

步驟如下所示:

  1. 將數據組織成一組對象,您的哈希圖非常適合補充和快速訪問。
  2. 編寫一些排序和過濾函數,它們將為您提供所需的數據子集。
  3. 編寫分頁器,以便您可以抓取數據集,然后根據一些修改后的參數獲取下一組數據
  4. 將您的“繪制/渲染”或“更新”方法替換為顯示符合輸入條件的當前 50 個集合的內容。

以下代碼應被視為可能有效的偽代碼:

// Represents each row in our table
function MyModelKlass(attributes) {
    this.attributes = attributes;
}

// Represents our table
function CollectionKlass() {
    this.children = [];
    this.visibleChildren = [];
    this.limit = 50;
}

CollectionKlass.prototype = {
    // accepts a callback to determine if things are in or out
    filter: function(callback) {
        // filter doesn't work in every browser
        // you can loop manually or user underscorejs
        var filteredObjects = this.children.filter(callback);

        this.visibleChildren = filteredObjects;
        this.filteredChildren = filteredObjects;
        this.showPage(0);
    },
    showPage: function(pageNumber) {
        // TODO: account for index out of bounds
        this.visibleChildren = this.filteredChildren.slice(
           pageNumber * this.limit,
           (pageNumber + 1) * this.limit
        );
    },
    // Another example mechanism, comparator is a function
    // sort is standard array sorting in JS
    sort: function(comparator) {
        this.children.sort(comparator);
    }
}

function render(el, collection, templateContent) {
    // this part is hard due to XSS
    // you need to sanitize all data being written or
    // use a templating language. I'll opt for 
    // handlebars style templating for this example.
    //
    // If you opt for no template then you need to do a few things.
    // Write then read all your text to a detached DOM element to sanitize
    // Create a detached table element and append new elements to it
    // with the sanitized data. Once you're done assembling attach the
    // element into the DOM. By attach I mean 'appendChild'.
    // That turns out to be mostly safe but pretty slow. 
    //
    // I'll leave the decisions up to you.
    var template = Handlebars.compile(templateContent);
    el.innerHTML(template(collection));
}

// Lets init now, create a collection and some rows
var myCollection = new CollectionKlass();

myCollection.children.push(new MyModelKlass({ 'a': 1 }));
myCollection.children.push(new MyModelKlass({ 'a': 2 }));

// filter on something...
myCollection.filter(function(child) {
    if (child.attributes.a === 1) {
        return false;
    }

    return true;
});

// this will throw an out of bounds error right now
// myCollection.showPage(2); 

// render myCollection in some element for some template
render(
    document.getElementById('some-container-for-the-table'), 
    myCollection,
    document.getElementById('my-template').innerHTML()
);

// In the HTML:

<script type="text/x-handlebars-template" id="my-template">
    <ul>
        {{#each visibleChildren}}
            <li>{{a}}</li>
        {{/each}}
    </ul>
</script>

使用AngularJS確實是一個好主意,它讓我們可以像這樣簡單地呈現您的行

<tr ng-repeat="row in rowArray">
  <td>{{row.id}}</td>
  <td>{{row.attr}}</td>
</tr>

您只需要將rowArray作為對象數組提供,例如{id: 1, attr: 'X'} ,請參閱ng-repeat指令的文檔 Angular的一大優勢在於其極其緊湊的代碼。

除此之外,Angular 還擁有強大的過濾器構建庫,可以在 HTML 中即時過濾和排序行:

<tr ng-repeat="row in rowArray | yourCustomFilter:parameters">
  <td>{{row.id}}</td>
  <td>{{row.attr}}</td>
</tr>

話雖如此,將 5K 行放入數組顯然會拖累性能。 這將在您的瀏覽器內存中創建一個巨大的 HTML,但是,它將不適合您的視口。 那么如果你無論如何都不能展示它,那么把它留在記憶中是沒有意義的。 相反,您只想在您的記憶中擁有可查看的部分以及可能的更多行。

看看Angular UI Utils提供的指令“Scroll until you drop”——它就是這么做的!

另一個答案中提到的分頁肯定是無限滾動的有效替代方案。 如果您想深入了解分頁與無限滾動的優缺點,網上有很多文章。


具體說到您的代碼,它還有其他性能拖累。 例如,在每次調用時,這個函數

document.getElementById(id).style.display="none"  

將通過id查找元素的 DOM,然后查找其屬性.style (如果 JavaScript 需要在Prototype 鏈中向上移動,這可能會造成拖累)。 您可以通過緩存指向display屬性的直接引用鏈接來提高性能,而這些鏈接正是您真正需要的。


編輯。 在這里緩存是指預先編譯一個帶有有趣屬性的hash鏈接id

hash[id] = document.getElementById(id).style.display

然后通過簡單的設置切換樣式:

hash[id] = 'none'
hash[id] = 'block'

這種計算hash假設你的元素都在DOM內部,這對性能不利,但還有更好的方法!

jQueryAngular這樣的庫,當然還有Angular :) 將允許您創建具有完整樣式屬性的 HTML 元素,但無需將它們附加到 DOM 這樣你就不會超載瀏覽器的容量。 但是你仍然可以緩存它們! 因此,您將像這樣緩存 HTML(但不是 DOM)元素及其顯示

elem[id] = $('<tr>' +
  '<td>' + id + '</td>' +
  '<td>' + attr + '</td>' +
</tr>');

display[id] = elem[id].style.display;

然后將您的元素附加/分離到 DOM 並使用顯示緩存更新它們的display屬性。

最后請注意,為了獲得更好的性能,您希望先將行連接到一個包中,然后再進行一次跳轉(而不是一個接一個地附加)。 原因是,每次更改 DOM 時,瀏覽器都必須進行大量重新計算才能正確調整所有其他 DOM 元素。 那里發生了很多事情,因此您希望盡可能減少這些重新計算。


發布編輯。

通過一個例子來說明,如果parentElement已經在你的 DOM 中,並且你想要附加一個新元素數組

elementArray = [rowElement1, ..., rowElementN]

你想這樣做的方式是:

var htmlToAppend = elementArray.join('');

parentElement.append(htmlToAppend);

而不是運行一個循環附加一個rowElement一次。

另一個好的做法是在附加之前hide您的parentElement ,然后僅在一切准備就緒時顯示。

我提出了一個您可能想查看的過濾解決方案。

特征

  • 幾乎可以立即處理 5000 行表*
  • 使用普通的舊 JavaScript; 不需要圖書館
  • 無需學習新語法; 使用它就像調用一個函數一樣簡單
  • 與您預先存在的表配合良好; 無需從頭開始
  • 不需要數據結構或緩存
  • 支持每個過濾器和多個過濾器的多個值
  • 支持包含和排除過濾
  • 如果您想在顯示之前應用過濾器,那么在與 DOM 分離的表上也能正常工作。

這個怎么運作

JavaScript 非常簡單。 它所做的只是為每個過濾器創建一個唯一的類名,並將其添加到與過濾器參數匹配的每一行中。 類名可用於確定給定過濾器當前正在過濾哪些行,因此無需將該信息存儲在數據結構中。 這些類共享一個公共前綴,因此它們都可以由相同的 CSS 選擇器定位以應用display: none聲明。 刪除過濾器就像從包含它的行中刪除其關聯的類名一樣簡單。


編碼

如果只想顯示第 2 列中值為“X”或“Y”的行,函數調用將如下所示:

addFilter(yourTable, 2, ['X','Y']);

這里的所有都是它的! 可以在下面的演示代碼中找到有關刪除過濾器的說明。


演示

下面的代碼片段中的演示允許您將具有任意數量值的任意數量的過濾器應用到 5000 行表中,就像 OP 描述的那樣,然后將它們刪除。 看起來代碼很多,但大部分只是為了設置演示界面。 如果您要在自己的代碼中使用此解決方案,您可能只需復制前兩個 js 函數(addFilter 和 removeFilter)和第一個 CSS 規則(帶有display: none規則)。

 /* The addFilter function is ready to use and should work with any table. You just need to pass it the following arguments: 1) a reference to the table 2) the numeric index of the column to search 3) an array of values to search for Optionally, you can pass it a boolean value as the 4th argument; if true, the filter will hide rows that DO contain the specified values rather than those that don't (it does the latter by default). The return value is an integer that serves as a unique identifier for the filter. You'll need to save this value if you want to remove the filter later. */ function addFilter(table, column, values, exclusive) { if(!table.hasAttribute('data-filtercount')) { table.setAttribute('data-filtercount', 1); table.setAttribute('data-filterid', 0); var filterId = 0; } else { var filterCount = parseInt(table.getAttribute('data-filtercount')) + 1, filterId = filterCount === 1 ? 0 : parseInt(table.getAttribute('data-filterid')) + 1; table.setAttribute('data-filtercount', filterCount); table.setAttribute('data-filterid', filterId); } exclusive = !!exclusive; var filterClass = 'filt_' + filterId, tableParent = table.parentNode, tableSibling = table.nextSibling, rows = table.rows, rowCount = rows.length, r = table.tBodies[0].rows[0].rowIndex; if(tableParent) tableParent.removeChild(table); for(; r < rowCount; r++) { if((values.indexOf(rows[r].cells[column].textContent.trim()) !== -1) === exclusive) rows[r].classList.add(filterClass); } if(tableParent) tableParent.insertBefore(table, tableSibling); return filterId; } /* The removeFilter function takes two arguments: 1) a reference to the table that has the filter you want to remove 2) the filter's ID number (ie the value that the addFilter function returned) */ function removeFilter(table, filterId) { var filterClass = 'filt_' + filterId, tableParent = table.parentNode, tableSibling = table.nextSibling, lastId = table.getAttribute('data-filterid'), rows = table.querySelectorAll('.' + filterClass), r = rows.length; if(tableParent) tableParent.removeChild(table); for(; r--; rows[r].classList.remove(filterClass)); table.setAttribute( 'data-filtercount', parseInt(table.getAttribute('data-filtercount')) - 1 ); if(filterId == lastId) table.setAttribute('data-filterid', parseInt(filterId) - 1); if(tableParent) tableParent.insertBefore(table, tableSibling); } /* THE REMAINING JS CODE JUST SETS UP THE DEMO AND IS NOT PART OF THE SOLUTION, though it does provide a simple example of how to connect the above functions to an interface. */ /* Initialize interface. */ (function() { var table = document.getElementById('hugeTable'), addFilt = function() { var exclusive = document.getElementById('filterType').value === '0' ? true : false, colSelect = document.getElementById('filterColumn'), valInputs = document.getElementsByName('filterValue'), filters = document.getElementById('filters'), column = colSelect.value, values = [], i = valInputs.length; for(; i--;) { if(valInputs[i].value.length) { values[i] = valInputs[i].value; valInputs[i].value = ''; } } filters.children[0].insertAdjacentHTML( 'afterend', '<div><input type="button" value="Remove">' + colSelect.options[colSelect.selectedIndex].textContent.trim() + (exclusive ? '; [' : '; everything but [') + values.toString() + ']</div>' ); var filter = filters.children[1], filterId = addFilter(table, column, values, exclusive); filter.children[0].addEventListener('click', function() { filter.parentNode.removeChild(filter); removeFilter(table, filterId); }); }, addFiltVal = function() { var input = document.querySelector('[name="filterValue"]'); input.insertAdjacentHTML( 'beforebegin', '<input name="filterValue" type="text" placeholder="value">' ); input.previousElementSibling.focus(); }, remFiltVal = function() { var input = document.querySelector('[name="filterValue"]'); if(input.nextElementSibling.name === 'filterValue') input.parentNode.removeChild(input); }; document.getElementById('addFilterValue').addEventListener('click', addFiltVal); document.getElementById('removeFilterValue').addEventListener('click', remFiltVal); document.getElementById('addFilter').addEventListener('click', addFilt); })(); /* Fill test table with 5000 rows of random data. */ (function() { var tbl = document.getElementById('hugeTable'), num = 5000, dat = [ 'a','b','c','d','e','f','g','h','i','j','k','l','m', 'n','o','p','q','r','s','t','u','v','w','x','y','z' ], len = dat.length, flr = Math.floor, rnd = Math.random, bod = tbl.tBodies[0], sib = bod.nextSibling, r = 0; tbl.removeChild(bod); for(; r < num; r++) { bod.insertAdjacentHTML( 'beforeend', '<tr><td>' + r + '</td><td>' + dat[flr(rnd() * len)] + '</td></tr>'); } tbl.insertBefore(bod, sib); })();
 [class*="filt_"] {display: none;} /* THIS RULE IS REQUIRED FOR THE FILTERS TO WORK!!! */ /* THE REMAINING CSS IS JUST FOR THE DEMO INTERFACE AND IS NOT PART OF THE SOLUTION. */ h3 {margin: 0 0 .25em 0;} [name="filterValue"] {width: 2.5em;} [class*="filt_"] {display: none;} #addFilter {margin-top: .5em;} #filters {margin-left: .5em;} #filters > div {margin-bottom: .5em;} #filters > div > input, select {margin-right: .5em;} #filters, #hugeTable { float: left; border: 1px solid black; padding: 0 .5em 0 .5em; white-space: nowrap; } #hugeTable {border-spacing: 0;} #hugeTable > thead > tr > th { padding-top: 0; text-align: left; } #hugeTable > colgroup > col:first-child {min-width: 4em;}
 <h3>Add Filter</h3> Column: <select id="filterColumn"> <option value="1">attr</option> <option value="0">id</option> </select> Action: <select id="filterType"> <option value="0">filter out</option> <option value="1">filter out everything but</option> </select> Value(s): <input id="addFilterValue" type="button" value="+" ><input id="removeFilterValue" type="button" value="-" ><input name="filterValue" type="text" placeholder="value"> <br> <input id="addFilter" type="button" value="Apply"> <hr> <table id="hugeTable"> <col><col> <thead> <tr><th colspan="2"><h3>Huge Table</h3></th></tr> <tr><th>id</th><th>attr</th></tr> </thead> <tbody> </tbody> </table> <div id="filters"> <h3>Filters</h3> </div>


*性能會有所不同,具體取決於應用於表格行和單元格的 CSS 數量,以及編寫該 CSS 時是否考慮了性能。 無論您使用什么過濾策略,除了加載較少的內容(正如其他人所建議的)之外,您無法做很多事情來使樣式過多或效率低下的表格表現良好。

看到這個鏈接可能會有所幫助,唯一的問題是它不是純 javascript,它也使用 angularjs。

    app.service("NameService", function($http, $filter){

  function filterData(data, filter){
    return $filter('filter')(data, filter)
  }

  function orderData(data, params){
    return params.sorting() ? $filter('orderBy')(data, params.orderBy()) : filteredData;
  }

  function sliceData(data, params){
    return data.slice((params.page() - 1) * params.count(), params.page() * params.count())
  }

  function transformData(data,filter,params){
    return sliceData( orderData( filterData(data,filter), params ), params);
  }

  var service = {
    cachedData:[],
    getData:function($defer, params, filter){
      if(service.cachedData.length>0){
        console.log("using cached data")
        var filteredData = filterData(service.cachedData,filter);
        var transformedData = sliceData(orderData(filteredData,params),params);
        params.total(filteredData.length)
        $defer.resolve(transformedData);
      }
      else{
        console.log("fetching data")
        $http.get("data.json").success(function(resp)
        {
          angular.copy(resp,service.cachedData)
          params.total(resp.length)
          var filteredData = $filter('filter')(resp, filter);
          var transformedData = transformData(resp,filter,params)

          $defer.resolve(transformedData);
        });  
      }

    }
  };
  return service;  
});

這是一個動態過濾器解決方案,它使用在keypress事件的輸入框中鍵入的字母來過濾表格。

雖然現在我在我當前的項目開發中使用 DataTables,但是如果你想要一個嚴格的javascript解決方案,這里就是它。 它可能不是最好的優化,但效果很好。

function SearchRecordsInTable(searchBoxId, tableId) {
    var searchText = document.getElementById(searchBoxId).value;
    searchText = searchText.toLowerCase();
    var targetTable = document.getElementById(tableId);
    var targetTableColCount;

    //Loop through table rows
    for (var rowIndex = 0; rowIndex < targetTable.rows.length; rowIndex++) {
        var rowData = '';

        //Get column count from header row
        if (rowIndex == 0) {
            targetTableColCount = targetTable.rows.item(rowIndex).cells.length;
            continue; //do not execute further code for header row.
        }

        //Process data rows. (rowIndex >= 1)
        for (var colIndex = 0; colIndex < targetTableColCount; colIndex++) {
            rowData += targetTable.rows.item(rowIndex).cells.item(colIndex).textContent;
            rowData = rowData.toLowerCase();
        }
        console.log(rowData);

        //If search term is not found in row data
        //then hide the row, else show
        if (rowData.indexOf(searchText) == -1)


            targetTable.rows.item(rowIndex).style.display = 'none';
        else
            targetTable.rows.item(rowIndex).style.display = '';
    }
}

干杯!!

渲染不僅要搜索,還要消耗大量時間和資源。 限制要顯示的行數,您的代碼可以像魅力一樣工作。 此外,如果您只打印有限的行,而不是隱藏和取消隱藏,那會更好。 您可以在我的開源庫https://github.com/thehitechpanky/js-bootstrap-tables 中查看它是如何完成的

    function _addTableDataRows(paramObjectTDR) {
    let { filterNode, limitNode, bodyNode, countNode, paramObject } = paramObjectTDR;
    let { dataRows, functionArray } = paramObject;
    _clearNode(bodyNode);
    if (typeof dataRows === `string`) {
        bodyNode.insertAdjacentHTML(`beforeend`, dataRows);
    } else {
        let filterTerm;
        if (filterNode) {
            filterTerm = filterNode.value.toLowerCase();
        }
        let serialNumber = 0;
        let limitNumber = 0;
        let rowNode;
        dataRows.forEach(currentRow => {
            if (!filterNode || _filterData(filterTerm, currentRow)) {
                serialNumber++;
                if (!limitNode || limitNode.value === `all` || limitNode.value >= serialNumber) {
                    limitNumber++;
                    rowNode = _getNode(`tr`);
                    bodyNode.appendChild(rowNode);
                    _addData(rowNode, serialNumber, currentRow, `td`);
                }
            }
        });
        _clearNode(countNode);
        countNode.insertAdjacentText(`beforeend`, `Showing 1 to ${limitNumber} of ${serialNumber} entries`);
    }
    if (functionArray) {
        functionArray.forEach(currentObject => {
            let { className, eventName, functionName } = currentObject;
            _attachFunctionToClassNodes(className, eventName, functionName);
        });
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM