如何使用纯 Javascript 过滤非常大的 bootstrap table

How to filter a very large bootstrap table using pure Javascript

我在 bootstrap 中构建了一个大的 table,大约 5,000 行 x 10 列,我需要过滤 table 特定属性,快速,仅使用 JavaScript。 table 有一个 id 列和一个属性列,即

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

为了加快过滤过程,我构建了一个散列table table,将属性映射回列 ID。例如,我有一个映射:

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

用户可以在搜索框中输入属性"X",散列table然后查找包含"X"的相应行(在本例中为2和4),然后通过映射操作调用以下函数:

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

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

这个过程仍然很慢,因为允许用户 select 多个属性(比如 X、Y)。

是否有更快的隐藏行的方法?

如果我能以某种方式将 table 与 DOM 分离,进行更改,然后重新连接,会不会更快?我如何在 javascript 中执行此操作?

还有其他 efficient/smarter 过滤方法吗?

谢谢:)

我会问

  • 为什么要自己写这段代码?根据个人经验,尝试在所有浏览器上进行有效过滤是一项非常重要的任务。
  • 如果您将此作为一种学习体验,请查看下面列出的包的源代码作为示例。
  • 对于 5000 行,进行服务器端筛选和排序会更有效率。然后用ajax更新显示的table.

我建议您考虑使用已经执行此操作的几个 JavaScript 软件包之一。除了下面的两个之外,还有更多的软件包。我将这两个展示为可用示例。

你最好的选择是不渲染所有这些东西并存储它们的对象版本,并且通过分页一次最多显示 50 行。在内存中存储那么多对象,在 JS 中是没有问题的。另一方面,将所有这些存储在 DOM 中会使浏览器崩溃。 5000 大约是浏览器在一台好的机器上可以做的事情的上限,同时保持良好的性能。如果您开始修改其中一些行并进行微调 ('hiding'、'showing'),事情肯定会变得更慢。

步骤类似于:

  1. 将数据组织成一个对象数组,您的哈希映射非常适合补充和快速访问目的。
  2. 编写一些排序和过滤函数,为您提供所需的数据子集。
  3. 写一个分页器,这样你就可以获取数据集,然后根据一些修改后的参数获取下一组数据
  4. 将您的 "draw/render" 或 "update" 方法替换为显示符合输入条件的当前 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>

参见 this link 它可能会有所帮助,唯一的问题是它不是纯粹的 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;  
});

使用AngularJS确实是个好主意, 这让我们可以将您的行呈现得如此简单

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

您只需要将 rowArray 作为对象数组提供,例如 {id: 1, attr: 'X'},请参阅 the documentation for ng-repeat directiveAngular的强大功能之一在于其极其紧凑的代码。

除其他外,Angular 还具有强大的 filter building library 功能,可以在您的 HTML:

中即时过滤和排序行
<tr ng-repeat="row in rowArray | yourCustomFilter:parameters">
  <td>{{row.id}}</td>
  <td>{{row.attr}}</td>
</tr>

话虽如此,将 5K 行放入数组中显然会拖累性能。这将在您的浏览器内存中创建一个巨大的 HTML,但是,它不适合您的视口。如果无论如何都不能显示它,那么将它保存在内存中就没有意义了。相反,您只想在内存中保留可见部分,再加上周围可能还有几行。

查看由 "Scroll till you drop" 提供的指令 Angular UI Utils - 正是这样做的!

另一个答案中提到的分页肯定是无限滚动的有效替代方案。如果您想深入研究,网络上有很多关于分页与无限滚动的优点和缺点的文章。


具体说到您的代码,它还有其他性能拖累。 例如,在每次调用时,此函数

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

将通过其 id 查找元素的 DOM,然后查找其 属性 .style(如果 JavaScript 需要在 原型链 中往上走。通过缓存指向 display 属性的直接引用链接,您可以在性能方面做得更好,这是您真正需要的。


编辑。 在这里缓存我的意思是预编译一个 hash 链接 id 与有趣的属性:

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

然后你通过简单的设置来切换风格:

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

这种计算hash的方法假设你的元素都在DOM里面,这对性能不利,但是有更好的方法!

jQuery 这样的库,当然还有 Angular :) 将允许您创建具有完整样式属性的 HTML 元素,但 无需将它们附加到DOM。这样您就不会超载浏览器的容量。但是你仍然可以缓存它们!因此,您将像这样缓存 HTML(但不是 DOM)Elements 及其 Display

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

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

然后将您的元素附加/分离到 DOM,并使用显示缓存更新它们的 display 属性。

最后请注意,为了获得更好的性能,您希望先将行连接成一个包,然后才在单个跳转中附加(而不是逐个附加)。原因是,每次更改 DOM 时,浏览器都必须进行大量重新计算才能正确调整所有其他 DOM 元素。那里有很多事情要做,所以你想尽可能地减少这些重新计算。


POST 编辑。

举例说明,如果 parentElement 已经在您的 DOM 中,并且您想附加一个新元素数组

elementArray = [rowElement1, ..., rowElementN]

您想要的方式是:

var htmlToAppend = elementArray.join('');

parentElement.append(htmlToAppend);

与 运行 不同,一个循环一次附加一个 rowElement

另一个好的做法是在附加之前 hide 您的 parentElement,然后仅在一切就绪时显示。

我提出了一个过滤解决方案,您可能想看看。

特征

  • 几乎可以立即处理 5000 行 table*
  • 使用普通的旧 JavaScript;不需要图书馆
  • 无需学习新语法;使用它就像调用函数一样简单
  • 与您先前存在的 table 配合得很好;无需从头开始
  • 不需要数据结构或缓存
  • 支持每个过滤器多个值和多个过滤器
  • 支持包容和排斥过滤
  • 如果您想在显示之前应用过滤器,
  • 在与 DOM 分离的 table 上同样有效。

工作原理

JavaScript很简单。它所做的只是为每个过滤器创建一个唯一的 class 名称,并将其添加到与过滤器参数匹配的每一行。 class 名称可用于确定给定过滤器当前正在过滤哪些行,因此无需将该信息存储在数据结构中。 classes 共享一个公共前缀,因此它们都可以被同一个 CSS 选择器定位以应用 display: none 声明。删除过滤器就像从包含它的行中删除其关联的 class 名称一样简单。


代码

如果您只想显示第 2 列中值为 "X" 或 "Y" 的行,则函数调用应如下所示:

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

仅此而已!可以在下面的演示代码中找到有关删除过滤器的说明。


演示

下面代码片段中的演示允许您将具有任意数量值的任意数量的过滤器应用到 5000 行 table,就像 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 (i.e. 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 应用于 table 行和单元格的程度,以及编写 CSS 时是否考虑了性能。无论您使用何种过滤策略,除了减少负载(正如其他人所建议的那样)之外,您都无法使样式过多或效率低下的 table 表现良好。

这是一个即时过滤器解决方案,它使用在 keypress 事件的输入框中键入的字母来过滤 table。

虽然现在我在我当前的项目开发中使用 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);
        });
    }
}