目标
我有一个带有大约70个元素的DOM(带有一些内容的div)。我需要移动和切换这些div的显示非常多,也很快。速度是最重要的事情之一。移动和切换这些div的触发器是一个搜索查询,类似于Google Instant,除了我第一次加载所有移动和切换的DOM元素(因此不再调用服务器)。
实施
我已经通过以下方式实现了这一点:在DOM旁边,我传递了一个表示div的JavaScript数组,以及它们的属性,如position,contents etcetera。这个数组就像是DOM的镜像。当用户开始输入时,我开始循环遍历数组,并根据div / object计算需要对其执行的操作。我实际上循环了几次这个数组:我首先检查是否需要查看div /对象,然后查看对象,然后是否需要查看内容,然后查看内容。
我在这些循环中做的一件事是设置DOM操作的标志。据我所知,读取和操作以及DOM是JavaScript中较慢的操作之一,与我正在做的其他事情(循环,读取和编写对象属性等)相比。我也做了一些剖析,证实了这个假设。因此,在每个角落,我都试图阻止“触摸”DOM以提高性能。在我的算法结束时,我再次循环,执行所有必要的DOM操作并重置标志以表示它们已被读取。对于跨浏览器兼容性,我使用jQuery实际执行DOM操作(选择,移动,切换)。我不使用jQuery循环遍历我的数组。
问题
我现在的问题是我认为我的代码和数据结构有点难看。我有这个 具有大量属性和标志的大型多维数组。我用函数调用函数调用函数重复循环它。当遇到问题时,我可以(仍然)稍微轻松地调试内容,但它不会感觉正确。
问题
是否存在针对此类问题的设计模式或常见解决方案?我怀疑我可以在数组和DOM之间实现某种智能耦合,我不需要显式设置标志和执行DOM操作,但我不知道这种耦合应该如何工作,或者它是否是一个好主意或者只会让事情变得复杂。
在解决这个问题时,我是否忽略了其他任何数据结构或算法原理?
谢谢!
更新 根据要求,我添加了我的代码,它大约有700行。注意:我没有污染全局命名空间,这些函数是在闭包中定义和使用的。
/**
* Applies the filter (defined by the currentQuery and to the cats array)
*
* -checks whether matching is needed
* -if needed does the matching
* -checks whether DOM action is needed
* -if needed executes DOM action
*
* cats is an array of objects representing categories
* which themselves contain an array of objects representing links
* with some attributes
*
* cats = (array) array of categories through which to search
* currentQuery = (string) with which to find matches within the cats
* previousQuery = (string) with previously-typed-in query
*
* no return values, results in DOM action and manipulation of cats array
*/
function applyFilter(cats,currentQuery, previousQuery) {
cats = flagIfMatchingIsNeededForCats(cats,currentQuery,previousQuery);
cats = matchCats(cats,currentQuery);
cats = flagIfMatchingIsNeededForLinks(cats,currentQuery,previousQuery);
cats = matchLinks(cats,currentQuery);
cats = flagIfDisplayToggleNeeded(cats);
if ( currentQuery.length > 0 ) {
cats = flagIfMoveNeeded(cats);
} else {
// move everything back to its original position
cats = flagMoveToOriginalPosition(cats);
}
// take action on the items that need a DOM action
cats = executeDomActions(cats);
}
/**
* Sets a flag on a category if it needs matching, parses and returns cats
*
* Loops through all categories and sets a boolean to signal whether they
* need matching.
*
* cats = (array) an array with all the category-objects in it
* currentQuery = (string) the currently typed-in query
* previousQuery = (string) the query that was previously typed in
*
* returns (array) cats, possibly in a different state
*/
function flagIfMatchingIsNeededForCats(cats,currentQuery,previousQuery) {
var newQueryIsLonger = isNewQueryLonger(currentQuery, previousQuery);
// check if matching is necessary for categories
for (var i = 0; i < cats.length; i++) {
cats[i].matchingNeeded = isMatchingNeededForCat(
cats[i].matches
,newQueryIsLonger
,currentQuery.length
,cats[i].noMatchFoundAtNumChars
);
}
return cats;
}
/**
* Whether the new query is longer than the previous one
*
* currentQuery = (string) the currently typed-in query
* previousQuery = (string) the query that was previously typed in
*
* returns (boolean) true/false
*/
function isNewQueryLonger(currentQuery, previousQuery) {
if (previousQuery == false) {
return true;
}
return currentQuery.length > previousQuery.length
}
/**
* Deduces if a category needs to be matched to the current query
*
* This function helps in improving performance. Matching is done using
* indexOf() which isn't slow of itself but preventing even fast processes
* is a good thing (most of the time). The function looks at the category,
* the current and previous query, then decides whether
* matching is needed.
*
* currentlyMatched = (boolean) on whether the boolean was matched to the previous query
* newQueryIsLonger = (boolean) whether the new query is longer
* queryLength = (int) the length of the current query
* noMatchFoundAtNumChars = (int) this variable gets set (to an int) for a
* category when it switches from being matched to being not-matched. The
* number indicates the number of characters in the first query that did
* not match the category. This helps in performance because we don't need
* to recheck the categoryname if it doesn't match now and the new query is
* even longer.
*
* returns (boolean) true/false
*/
function isMatchingNeededForCat(currentlyMatched, newQueryIsLonger ,queryLength ,noMatchFoundAtNumChars) {
if (typeof(currentlyMatched) == 'undefined') {
// this happens the first time we look at a category, for all
// categories this happens with an empty query and that matches with
// everything
currentlyMatched = true;
}
if (currentlyMatched && newQueryIsLonger) {
return true;
}
if (!currentlyMatched && !newQueryIsLonger) {
// if currentlyMatched == false, we always have a value for
// noMatchFoundAtNumChars
// matching is needed if the first "no-match" state was found
// at a number of characters equal to or bigger than
// queryLength
if ( queryLength < noMatchFoundAtNumChars ) {
return true;
}
}
return false;
}
/**
* Does matching on categories for all categories that need it.
*
* Sets noMatchFoundAtNumChars to a number if the category does not match.
* Sets noMatchFoundAtNumChars to false if the category matches once again.
*
* cats = (array) an array with all the category-objects in it
* currentQuery = (string) the currently typed-in query
*
* returns (array) cats, possibly in a different state
*/
function matchCats(cats,currentQuery) {
for (var i = 0; i < cats.length; i++) {
if (cats[i].matchingNeeded) {
cats[i].matches = categoryMatches(cats[i],currentQuery);
// set noMatchFoundAtNumChars
if (cats[i].matches) {
cats[i].noMatchFoundAtNumChars = false;
} else {
cats[i].noMatchFoundAtNumChars = currentQuery.length;
}
}
}
return cats;
}
/**
* Check if the category name matches the query
*
* A simple indexOf call to the string category_name
*
* category = (object) a category object
* query = (string) the query
*
* return (boolean) true/false
*/
function categoryMatches(category,query) {
catName = category.category_name.toLowerCase();
if (catName.indexOf(query) !== -1 ) {
return true;
}
return false;
}
/**
* Checks links to see whether they need matching
*
* Loops through all cats, selects the non-matching, for every link decides
* whether it needs matching
*
* cats = (array) an array with all the category-objects in it
* currentQuery = the currently typed-in query
* previousQuery = the query that was previously typed in
*
* returns (array) cats, possibly in a different state
*/
function flagIfMatchingIsNeededForLinks(cats,currentQuery,previousQuery) {
var newQueryIsLonger = isNewQueryLonger(currentQuery, previousQuery);
for (var i = 0; i < cats.length; i++) {
if (!cats[i].matches) { // only necessary when cat does not match
for (var k = 0; k < cats[i].links.length; k++) {
cats[i].links[k].matchingNeeded = isMatchingNeededForLink(
cats[i].links[k].matches
,newQueryIsLonger
,currentQuery.length
,cats[i].links[k].noMatchFoundAtNumChars
);
}
}
}
return cats;
}
/**
* Checks whether matching is needed for a specific link
*
* This function helps in improving performance. Matching is done using
* indexOf() for every (relevant) link property, this function helps decide
* whether that *needs* to be done. The function looks at some link
* properties, the current and previous query, then decides whether
* matching is needed for the link.
*
* currentlyMatched = (boolean) on whether the boolean was matched to the previous query
* newQueryIsLonger = (boolean) whether the new query is longer
* queryLength = (int) the length of the current query
* noMatchFoundAtNumChars = (int) this variable gets set (to an int) for a
* link when it switches from being matched to being not-matched. The
* number indicates the number of characters in the first query that did
* not match the link. This helps in performance because we don't need
* to recheck the link properties in certain circumstances.
*
* return (boolean) true/false
*/
function isMatchingNeededForLink(currentlyMatched, newQueryIsLonger ,queryLength ,noMatchFoundAtNumChars) {
if (typeof(currentlyMatched) == 'undefined') {
// this happens to a link the first time a cat does not match and
// we want to scan the links for matching
return true;
}
if (currentlyMatched && newQueryIsLonger) {
return true;
}
if (!currentlyMatched && !newQueryIsLonger) {
// if currentlyMatched == false, we always have a value for
// noMatchFoundAtNumChars
// matching is needed if the first "no-match" state was found
// at a number of characters equal to or bigger than
// queryLength
if ( queryLength < noMatchFoundAtNumChars ) {
return true;
}
}
return false;
}
/**
* Does matching on links for all links that need it.
*
* Sets noMatchFoundAtNumChars to a number if the link does not match.
* Sets noMatchFoundAtNumChars to false if the link matches once again.
*
* cats = (array) an array with all the category-objects in it
* currentQuery = (string) the currently typed-in query
*
* returns (array) cats, possibly in a different state
*/
function matchLinks(cats,currentQuery) {
for (var i = 0; i < cats.length; i++) {
// category does not match, check if links in the category match
if (!cats[i].matches) {
for (var k = 0; k < cats[i].links.length; k++) {
if (cats[i].links[k].matchingNeeded) {
cats[i].links[k].matches = linkMatches(cats[i].links[k],currentQuery);
}
// set noMatchFoundAtNumChars
if (cats[i].links[k].matches) {
cats[i].links[k].noMatchFoundAtNumChars = false;
} else {
cats[i].links[k].noMatchFoundAtNumChars = currentQuery.length;
}
}
}
}
return cats;
}
/**
* Check if any of the link attributes match the query
*
* Loops through all link properties, skips the irrelevant ones we use for filtering
*
* category = (object) a category object
* query = (string) the query
*
* return (boolean) true/false
*/
function linkMatches(link,query) {
for (var property in link) {
// just try to match certain properties
if (
!( // if it's *not* one of the following
property == 'title'
|| property == 'label'
|| property == 'url'
|| property == 'keywords'
|| property == 'col'
|| property == 'row'
)
){
continue;
}
// if it's an empty string there's no match
if( !link[property] ) {
continue;
}
var linkProperty = link[property].toLowerCase();
if (linkProperty.indexOf(query) !== -1){
return true;
}
}
return false;
}
/**
* Flags if toggling of display is needed for a category.
*
* Loops through all categories. If a category needs some DOM
* action (hiding/showing) it is flagged for action. This helps in
* performance because we prevent unnecessary calls to the DOM (which are
* slow).
*
* cats = (array) an array with all the category-objects in it
*
* returns (array) cats, possibly in a different state
*/
function flagIfDisplayToggleNeeded(cats) {
for (var i = 0; i < cats.length; i++) {
// this happens the first time we look at a category
if (typeof(cats[i].currentlyDisplayed) == 'undefined') {
cats[i].currentlyDisplayed = true;
}
var visibleLinks = 0;
// a cat that matches, all links need to be shown
if (cats[i].matches) {
visibleLinks = cats[i].links.length;
} else {
// a cat that does not match
for (var k = 0; k < cats[i].links.length; k++) {
if (cats[i].links[k].matches) {
visibleLinks++;
}
}
}
// hide/show categories if they have any visible links
if (!cats[i].currentlyDisplayed && visibleLinks > 0 ) {
cats[i].domActionNeeded = 'show';
} else if( cats[i].currentlyDisplayed && visibleLinks == 0 ){
cats[i].domActionNeeded = 'hide';
}
}
return cats;
}
/**
* Flags categories to be moved to other position.
*
* Loops through all categories and looks if they are distributed properly.
* If not it moves them to another position. It remembers the old position so
* it can get the categories back in their original position.
*
* cats = (array) an array with all the category-objects in it
*
* returns (array) cats, possibly in a different state
*/
function flagIfMoveNeeded(cats) {
var numCats, numColumns, displayedCats, i, moveToColumn, tmp;
numColumns = getNumColumns(cats);
numDisplayedCats = getNumDisplayedCats(cats);
columnDistribution = divideInPiles(numDisplayedCats, numColumns);
// optional performance gain: only move stuff when necessary
// think about this some more
// we convert the distribution in columns to a table so we get columns
// and positions
catDistributionTable = convertColumnToTableDistribution(columnDistribution);
// sort the categories, highest positions first
// catPositionComparison is a function to do the sorting with
// we could improve performance by doing this only once
cats = cats.sort(catPositionComparison);
for (i = 0; i < cats.length; i += 1) {
if( categoryWillBeDisplayed(cats[i]) ){
tmp = getNewPosition(catDistributionTable); // returns multiple variables
catDistributionTable = tmp.catDistributionTable;
cats[i].moveToColumn = tmp.moveToColumn;
cats[i].moveToPosition = tmp.moveToPosition;
} else {
cats[i].moveToColumn = false;
cats[i].moveToPosition = false;
}
}
return cats;
}
/**
* A comparison function to help the sorting in flagIfMoveNeeded()
*
* This function compares two categories and returns an integer value
* enabling the sort function to work.
*
* cat1 = (obj) a category
* cat2 = (obj) another category
*
* returns (int) signaling which category should come before the other
*/
function catPositionComparison(cat1, cat2) {
if (cat1.category_position > cat2.category_position) {
return 1; // cat1 > cat2
} else if (cat1.category_position < cat2.category_position) {
return -1; // cat1 < cat2
}
// the positions are equal, so now compare on column, if we need the
// performance we could skip this
if (cat1.category_column > cat2.category_column) {
return 1; // cat1 > cat2
} else if (cat1.category_column < cat2.category_column) {
return -1; // cat1 < cat2
}
return 0; // position and column are equal
}
/**
* Checks if a category will be displayed for the currentQuery
*
* cat = category (object)
*
* returns (boolean) true/false
*/
function categoryWillBeDisplayed(cat) {
if( (cat.currentlyDisplayed === true && cat.domActionNeeded !== 'hide')
||
(cat.currentlyDisplayed === false && cat.domActionNeeded === 'show')
){
return true;
} else {
return false;
}
}
/**
* Gets the number of unique columns in all categories
*
* Loops through all cats and saves the columnnumbers as keys, insuring
* uniqueness. Returns the number of
*
* cats = (array) of category objects
*
* returns (int) number of unique columns of all categories
*/
function getNumColumns(cats) {
var columnNumber, uniqueColumns, numUniqueColumns, i;
uniqueColumns = [];
for (i = 0; i < cats.length; i += 1) {
columnNumber = cats[i].category_column;
uniqueColumns[columnNumber] = true;
}
numUniqueColumns = 0;
for (i = 0; i < uniqueColumns.length; i += 1) {
if( uniqueColumns[i] === true ){
numUniqueColumns += 1
}
}
return numUniqueColumns;
}
/**
* Gets the number of categories that will be displayed for the current query
*
* cats = (array) of category objects
*
* returns (int) number of categories that will be displayed
*/
function getNumDisplayedCats(cats) {
var numDisplayedCats, i;
numDisplayedCats = 0;
for (i = 0; i < cats.length; i += 1) {
if( categoryWillBeDisplayed(cats[i]) ){
numDisplayedCats += 1;
}
}
return numDisplayedCats;
}
/**
* Evenly divides a number of items into piles
*
* Uses a recursive algorithm to divide x items as evenly as possible over
* y piles.
*
* items = (int) a number of items to be divided
* piles = (int) the number of piles to divide items into
*
* return an array with numbers representing the number of items in each pile
*/
function divideInPiles(items, piles) {
var averagePerPileRoundedUp, rest, pilesDivided;
pilesDivided = [];
if (piles === 0) {
return false;
}
averagePerPileRoundedUp = Math.ceil(items / piles);
pilesDivided.push(averagePerPileRoundedUp);
rest = items - averagePerPileRoundedUp;
if (piles > 1) {
pilesDivided = pilesDivided.concat(divideInPiles(rest, piles - 1)); // recursion
}
return pilesDivided;
}
/**
* Converts a column distribution to a table
*
* Receives a one-dimensional distribution array and converts it to a two-
* dimensional distribution array.
*
* columnDist (array) an array of ints, example [3,3,2]
*
* returns (array) two dimensional array, rows with "cells"
* example: [[true,true,true],[true,true,true],[true,true,false]]
* returns false on failure
*/
function convertColumnToTableDistribution(columnDist) {
'use strict';
var numRows, row, numCols, col, tableDist;
if (columnDist[0] === 'undefined') {
return false;
}
// the greatest number of items are always in the first column
numRows = columnDist[0];
numCols = columnDist.length;
tableDist = []; // we
for (row = 0; row < numRows; row += 1) {
tableDist.push([]); // add a row
// add "cells"
for (col = 0; col < numCols; col += 1) {
if (columnDist[col] > 0) {
// the column still contains items
tableDist[row].push(true);
columnDist[col] -= 1;
} else {
tableDist[row][col] = false;
}
}
}
return tableDist;
}
/**
* Returns the next column and position to place a category in.
*
* Loops through the table to find the first position that can be used. Rows
* and positions have indexes that start at zero, we add 1 in the return
* object.
*
* catDistributionTable = (array) of rows, with positions in them
*
* returns (object) with the mutated catDistributionTable, a column and a
* position
*/
function getNewPosition(catDistributionTable) {
var numRows, row, col, numCols, moveToColumn, moveToPosition;
numRows = catDistributionTable.length;
findposition:
for (row = 0; row < numRows; row += 1) {
numCols = catDistributionTable[row].length;
for ( col = 0; col < numCols; col += 1) {
if (catDistributionTable[row][col] === true) {
moveToColumn = col;
moveToPosition = row;
catDistributionTable[row][col] = false;
break findposition;
}
}
}
// zero-indexed to how it is in the DOM, starting with 1
moveToColumn += 1;
moveToPosition += 1;
return {
'catDistributionTable' : catDistributionTable
,'moveToColumn' : moveToColumn
,'moveToPosition' : moveToPosition
};
}
/**
* Sets the target position of a category to its original location
*
* Each category in the DOM has attributes defining their original position.
* After moving them around we might want to move them back to their original
* position, this function flags all categories to do just that.
*
* cats = (array) of category objects
*
* All of the possible return values
*/
function flagMoveToOriginalPosition(cats) {
for (i = 0; i < cats.length; i += 1) {
cats[i].moveToColumn = cats.category_column;
cats[i].moveToPosition = cats.category_position;
}
return cats;
}
/**
* Execute DOM actions for the items that need DOM actions
*
* Parses all categories, executes DOM actions on the categories that
* require a DOM action.
*
* cats = (array) an array with all the category-objects in it
*
* no return values
*/
function executeDomActions(cats) {
for (var i = 0; i < cats.length; i++) {
var category_id = cats[i].category_id;
// toggle display of columns
if (cats[i].domActionNeeded == 'show') {
showCategory(category_id);
cats[i].currentlyDisplayed = true;
}
if (cats[i].domActionNeeded == 'hide') {
hideCategory(category_id);
cats[i].currentlyDisplayed = false;
}
cats[i].domActionNeeded = false;
// for every currentlyDisplayed category move it to new location
// if necessary
if (cats[i].currentlyDisplayed && cats[i].moveToColumn !== false) {
cats[i] = moveCat(cats[i]);
}
}
return cats;
}
/**
* Show a certain category
*
* category_id = (int) the id of the category that needs to be shown
*
* no return values
*/
function showCategory(category_id) {
$('#' + category_id).show();
}
/**
* Hide a certain category
*
* category_id = (int) the id of the category that needs to be hidden
*
* no return values
*/
function hideCategory(category_id) {
$('#' + category_id).hide();
}
/**
* Moves a category to the position set in its attributes
*
* A category can have attributes defining the column and position (or row)
* this function moves the category to the correct column and position.
*
* cat = (object) category
*
* returns (object) category
*/
function moveCat(cat) {
var columnSelector, catSelector;
columnSelector = '#column' + cat.moveToColumn + ' .column_inner' + ' .hiddenblocks';
catSelector = '#' + cat.category_id;
$(columnSelector).prepend($(catSelector));
// reset target coordinates
cat.moveToColumn = false;
cat.moveToPosition = false;
return cat;
}
答案 0 :(得分:2)
评论和格式化JavaScript,kudos先生!
首先,它的接口就像你的用例一样,非常适合SQL数据库查询。将查询发送到数据库并获取类别ID和位置将比当前实现简单得多。我假设您在所有客户端都这样做,因为您无法访问数据库,您的数据是相当静态的,或者您对数据库的实时速度没有信心。
要加快当前实现小写的速度,并事先将所有链接数据属性连接到一个属性中。
function linkMatches(link,query) {
if (link["ConcatenatedLCasedProperties"].indexOf(query) !== -1){
return true;
}
return false;
}
编辑这是您的divideInPiles函数的更快/更有效的版本。
function divideInPiles(items, piles) {
var result = [];
var perPile = Math.floor(items/piles);
var leftOver = items % piles;
if(piles == 0)
return false;
for(var x=0; x<piles; x++)
result.push(perPile + (--leftOver >= 0 ? 1: 0));
return result;
}
答案 1 :(得分:1)
我认为那里真的可以使用树形结构。您也可以尝试实现一些图形算法的操作。此外,当你存储最流行的信息时,认为在树的每个级别上都有一些隐藏的div是合理的,你可以在必要时显示它而不是使用div内容进行操作。
但是,认为它需要为您指定更多细节的任务。一些真实案例可能非常有用。
答案 2 :(得分:1)
由于DOM-Operations成本很高,因此您应该从树中分离元素,处理它们,然后将它们重新附加到DOM。这可以通过JQuery的.detach()轻松完成。
我不知道你的数据结构,但最快的循环是带有计数器的普通循环。请记住在变量中存储任何可能的长度值,以便不在每个循环中查找长度。