Как фильтровать очень большую таблицу начальной загрузки, используя чистый Javascript
Я построил большую таблицу в бутстрапе, около 5000 строк по 10 столбцов, и мне нужно отфильтровать таблицу для определенных атрибутов, быстро, используя только JavaScript. Таблица имеет как столбец идентификатора, так и столбец атрибутов, т.е.
id | attr | ...
---------------
2 | X | ...
3 | Y | ...
4 | X | ...
Чтобы ускорить процесс фильтрации, я построил таблицу хеш-таблицы, которая сопоставляет атрибуты с идентификаторами столбцов. Так, например, у меня есть отображение:
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?
Существуют ли другие более эффективные/более умные способы фильтрации?
Спасибо:)
Ответы
Ответ 1
Использование 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 - он делает именно это!
Разбиение страницы, как указано в другом ответе, безусловно, является верной альтернативой бесконечному свитку. В Интернете много написано о сильных и слабых сторонах разбиения на страницы и бесконечный свиток, если вы хотите вникать в это.
Говоря конкретно о вашем коде, он имеет другие возможности перетаскивания.
Например, для каждого вызова эта функция
document.getElementById(id).style.display="none"
будет искать DOM для элемента по его id
, а затем будет искать свое свойство .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) Элементы и их Показать следующим образом:
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
перед присоединением, а затем показывается только когда все будет готово.
Ответ 2
Я бы спросил
- Почему вы хотите написать этот код для себя? Из личного опыта, попытка фильтровать эффективно и во всех браузерах является нетривиальной задачей.
- Если вы делаете это как учебное задание, посмотрите на источник пакетов, перечисленных ниже, в качестве примеров.
- С 5000 строк было бы более эффективно выполнять фильтрацию и сортировку на стороне сервера. Затем используйте ajax для обновления отображаемой таблицы.
Я бы предложил вам использовать один из нескольких пакетов JavaScript, которые уже делают это. Есть еще много пакетов, которые представлены ниже. Я показываю эти два примера того, что доступно.
Ответ 3
Ваш лучший вариант - не отображать все эти вещи и хранить версии объектов из них и показывать только максимум 50 строк за раз с помощью разбивки на страницы. Хранение многих объектов в памяти, в JS не проблема. С другой стороны, сохранение всех тех, кто находится в DOM, заставит браузеры встать на колени. 5000 находится вокруг верхней границы того, что браузер может делать на хорошей машине, сохраняя при этом достойную производительность. Если вы начнете изменять некоторые из этих строк и настраивать вещи ( "скрывать", "показывать" ), то определенно будет еще медленнее.
Этапы выглядят примерно так:
- Организуйте данные в массив объектов, ваша хэш-карта отлично подходит для дополнительного и быстрого доступа.
- Напишите некоторые функции сортировки и фильтрации, которые предоставят вам нужные вам подмножества данных.
- Назовите paginator, чтобы вы могли захватить наборы данных, а затем получить следующий набор на основе некоторых измененных параметров
- Замените метод "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>
Ответ 4
Я взломал фильтрующее решение, которое вы, возможно, захотите проверить.
Функции
- может обрабатывать 5000 строк таблицы почти мгновенно *
- использует простой старый JavaScript; нет необходимости в библиотеках
- нет нового синтаксиса для изучения; используя его так же просто, как вызвать функцию
- отлично работает с вашим предыдущим столом; не нужно начинать с нуля.
- нет структур данных или требуется кэширование
- поддерживает несколько значений для каждого фильтра и нескольких фильтров.
- поддерживает инклюзивную и эксклюзивную фильтрацию
- работает так же, как и на таблице, отделенной от DOM, если вы хотите применить фильтры перед ее отображением.
Как это работает
JavaScript очень прост. Все, что он делает, это создать уникальное имя класса для каждого фильтра и добавить его в каждую строку, соответствующую параметрам фильтра. Имена классов могут использоваться для определения того, какие строки данный фильтр фильтрует в настоящее время, поэтому нет необходимости хранить эту информацию в структуре данных. Классы имеют общий префикс, поэтому все они могут быть нацелены одним и тем же селектором CSS для применения объявления display: none
. Удаление фильтра так же просто, как удаление связанного имени класса из строк, которые у него есть.
Код
Если вы хотите показать только строки, которые имеют значение "X" или "Y" в столбце 2, вызов функции будет выглядеть примерно так:
addFilter(yourTable, 2, ['X','Y']);
Вот и все! Инструкции по удалению фильтра можно найти в демо-коде ниже.
Demo
Демонстрация в нижеприведенном фрагменте кода позволяет применять любое количество фильтров с любым количеством значений к таблице строк 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 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>
Ответ 5
см. эта ссылка, это может помочь, единственная проблема заключается не в чистом 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;
});
Ответ 6
Вот фильтр на лету фильтра, который фильтрует таблицу, используя буквы, введенные в поле ввода, в событии 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 = '';
}
}
Ура!!