Как фильтровать очень большую таблицу начальной загрузки, используя чистый Javascript

Я построил большую таблицу в bootstrap, около 5,000 строк x 10 столбцов, и мне нужно отфильтровать таблицу для определенных атрибутов, быстро, используя только JavaScript. Таблица имеет как столбец id, так и столбец атрибута, т. е.

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

чтобы сделать процесс фильтрации быстрым, я построил таблицу hashtable, которая отображает атрибуты обратно в идентификаторы столбцов. Так, например, у меня есть отображение:

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

пользователь может ввести атрибут "X" в поле поиска, хеш-таблицы затем просматривает соответствующие строки, содержащие " X " (2 и 4 в этом случае), а затем вызывает следующие функции с помощью операции map:

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

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

этот процесс все еще довольно медленный, так как пользователю разрешено выбирать несколько атрибутов (скажем,X, Y).

есть ли более быстрый способ скрытия строк?

было бы быстрее, если бы я мог каким-то образом отделить таблицу от DOM, внести изменения, а затем снова прикрепить? Как мне это сделать? в JavaScript?

есть ли другие более эффективные / умные способы выполнения фильтрации?

спасибо :)

6 ответов


используя 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 также имеет мощный фильтр здании библиотеки для фильтрации и сортировки строк fly прямо внутри вашего HTML:

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

сказав это, это будет явно перетаскивание производительности, чтобы бросить 5K строк в Ваш массив. Это создаст огромный HTML в памяти Вашего браузера, который, однако, не будет вписываться в ваш видовой экран. Тогда нет смысла иметь его в памяти, если вы все равно не можете его показать. Вместо этого вы хотите иметь только видимую часть в своей памяти плюс, возможно, еще несколько строк.

взгляните на директиву " прокрутите, пока не упадете" предоставлено угловой UI Utils - это делает именно это!

разбиение на страницы, как упоминалось в другом ответе, безусловно, является действительной альтернативой бесконечному свитку. В интернете много написано о сильных и слабых сторонах pagination vs infinite scroll, если вы хотите вникнуть в это.


говоря о вашем коде конкретно, у него есть другие перетаскивания производительности. Например, при каждом вызове эта функция

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

будет выглядеть DOM для элемента по его id, а потом будет искать свое свойство .style (что может быть перетаскиванием, если JavaScript должен идти высоко в прототип цепи). Вы можете сделать гораздо лучшую производительность, кэшируя прямые ссылки на display свойства, которые вам действительно нужны.


правка. Под кэшированием здесь я подразумеваю предварительную компиляцию a 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.

чтобы проиллюстрировать примером, если parentElement уже находится в вашем DOM, и вы хотите прикрепить массив новых элементов

elementArray = [rowElement1, ..., rowElementN]

то, как вы хотите это сделать:

var htmlToAppend = elementArray.join('');

parentElement.append(htmlToAppend);

в отличие от запуска цикла крепления один rowElement одновременно.

еще одна хорошая практика -hide код parentElement перед прикреплением, то только показать, когда все готово.


прошу

  • почему вы хотите оставить этот код для себя? Из личного опыта, попытка эффективно фильтровать и на всех браузерах является нетривиальной задачей.
  • Если вы делаете это как опыт обучения, то посмотрите на источник пакетов, перечисленных ниже в качестве примеров.
  • С 5000 строками было бы более эффективно выполнять фильтрацию и сортировку на стороне сервера. Затем используйте ajax для обновления отображаемой таблицы.

Я бы советуем посмотреть, используя один из нескольких пакетов JavaScript, которые уже делают это. Есть еще много пакетов, которые два ниже. Я показываю эти два примера того, что доступно.

  • http://datatables.net/ - это очень полнофункциональный пакет, который обрабатывает фильтрацию и сортировку на стороне клиента и сервера.
  • http://www.listjs.com/ - легкая фильтрация и сортировка на стороне клиента пакет.

ваш лучший вариант-не отображать все эти вещи и хранить их объектные версии и показывать не более 50 строк за раз через разбиение на страницы. Хранение этого количества объектов в памяти в JS не является проблемой. Хранение всех тех, кто в DOM, с другой стороны, поставит браузеры на колени. 5000 - это примерно верхняя граница того, что браузер может сделать на хорошей машине, сохраняя при этом приличную производительность. Если вы начнете изменять некоторые из этих строк и настраивать вещи ("скрывать", "показывать") вещи определенно станет еще медленнее.

шаги будут выглядеть примерно так:

  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>

Я приготовил фильтрующее решение, которое вы можете проверить.

особенности

  • может обрабатывать таблицу строк 5000 почти мгновенно*
  • использует простой старый JavaScript; нет необходимости в библиотеках
  • нет нового синтаксиса для изучения; использование его так же просто, как вызов функции
  • отлично работает с вашей существующей таблицей; нет необходимости начинать с нуля
  • нет структур данных и кэширования требуется
  • поддерживает несколько значений на фильтр и несколько фильтров
  • поддерживает инклюзивный и эксклюзивный фильтрации
  • работает так же хорошо на таблице, которая отделена от DOM, если вы хотите применить фильтры перед отображением.

как это работает

JavaScript очень прост. Все, что он делает, это создает уникальное имя класса для каждого фильтра и добавляет его в каждую строку, которая соответствует фильтру параметры. Имена классов можно использовать для определения строк, которые данный фильтр фильтрует в данный момент, поэтому нет необходимости хранить эту информацию в структуре данных. Классы имеют общий префикс, поэтому все они могут быть нацелены на один и тот же селектор CSS для применения display: none декларации. Удаление фильтра так же просто, как удаление Его связанного имени класса из строк, которые имеют его.


Код

если вы хотите показать только те строки, которые имейте значение " X "или" Y " в столбце 2, вызов функции будет выглядеть примерно так:

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 написан с учетом производительности. Любые фильтры стратегия, которую вы используете, не так много вы можете сделать, чтобы заставить сильно или неэффективно стилизованную таблицу работать хорошо, кроме как загружать ее меньше (как предлагали другие).


посмотреть этой 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;  
});

здесь на лету решение фильтра, которые фильтруют таблицу с помощью букв, введенных в поле ввода на 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 = '';
    }
}

Ура!!