繁体   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