如何使用纯Javascript过滤非常大的引导表

时间:2015-04-10 22:23:35

标签: javascript twitter-bootstrap dom infinite-scroll

我在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中执行此操作?

还有其他更有效/更智能的过滤方法吗?

谢谢:)

7 个答案:

答案 0 :(得分:6)

我会问

  • 为什么要为自己编写此代码?根据个人经验,尝试在所有浏览器上高效过滤是一项非常重要的任务。
  • 如果您将此作为学习经历,请查看下面列出的软件包来源作为示例。
  • 使用5000行,进行服务器端过滤和排序会更有效。然后使用ajax更新显示的表格。

我建议您使用已经执行此操作的多个JavaScript包中的一个。下面两个包有更多的包。我将这两个作为可用内容的示例。

答案 1 :(得分:4)

使用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,但是,它不适合您的视口。如果你无论如何都无法显示它,那就没有必要把它放在记忆中。相反,你只想让你的记忆中的可视部分加上可能还有几行。

看一下指令&#34;滚动直到你放弃&#34;由...提供 Angular UI Utils - 确实如此!

另一个答案中提到的分页肯定是无限卷轴的有效替代品。如果你想深入了解那么,网上有很多关于分页与无限卷轴的优缺点的文章。


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

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

将通过id查找元素的DOM,然后查找其属性.style(如果JavaScript需要在中高位运行,则可能会拖累它原型链)。通过将直接引用链接缓存到您真正需要的display属性,可以更好地提高性能。


EDIT。 通过在此缓存,我的意思是预先编译hash链接id与有趣的属性:

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

然后通过简单设置切换样式:

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

这种计算hash的方式假设你的元素都在DOM中,这对性能有害,但有更好的方法!

jQuery等图书馆,当然还有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元素。那里有很多,所以你想尽可能地减少那些重新计算。


POST EDIT。

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

elementArray = [rowElement1, ..., rowElementN]

你想要的方式是:

var htmlToAppend = elementArray.join('');

parentElement.append(htmlToAppend);

而不是运行一次附加一个rowElement的循环。

另一个好习惯是在hide附加parentElement之前,然后只显示所有内容都准备就绪。

答案 2 :(得分:3)

您最好的选择是不渲染所有这些内容并存储它们的对象版本,并且一次只能通过分页显示最多50行。将许多对象存储在内存中,在JS中没有问题。另一方面,将所有这些存储在DOM中将使浏览器瘫痪。 5000在浏览器可以在良好的机器上执行的上限附近,同时保持良好的性能。如果你开始修改其中一些行并调整一些东西(&#39;隐藏&#39;,&#39;显示&#39;)事情肯定会变得更慢。

步骤看起来像:

  1. 将数据整理到一个对象数组中,您的哈希映射非常适合补充和快速访问。
  2. 编写一些排序和过滤功能,为您提供所需的数据子集。
  3. 编写一个分页器,这样你就可以获取数据集,然后根据一些修改后的参数获取下一组
  4. 替换你的&#34;绘制/渲染&#34;或者&#34;更新&#34;显示当前50的符合输入条件的东西的方法。
  5. 以下代码应被视为可能有效的伪代码:

    // 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>
    

答案 3 :(得分:2)

我制作了一个您可能想要查看的过滤解决方案。

功能

  • 几乎可以立即处理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 (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数量,以及该CSS是否在考虑性能的情况下编写。无论你使用什么样的过滤策略,你都无法做出很好的或效率低下的表格,除了加载较少的表格之外(正如其他人所建议的那样)。

答案 4 :(得分:1)

请参阅this链接,这可能有所帮助,唯一的问题是它不是纯粹的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;  
});

答案 5 :(得分:0)

以下是即时过滤解决方案,使用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 = '';
    }
}

干杯!!

答案 6 :(得分:-1)

渲染不仅要进行搜索,而且会占用大量时间和资源。限制要显示的行数,您的代码可以像超级按钮一样工作。另外,如果只打印有限的行,则与其隐藏和取消隐藏,不如将其隐藏起来。您可以在我的开源库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);
        });
    }
}