如何改进这种JavaScript DOM操作数据结构/算法?

时间:2011-12-09 10:22:46

标签: javascript algorithm data-structures

目标

我有一个带有大约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;
}

3 个答案:

答案 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()轻松完成。

我不知道你的数据结构,但最快的循环是带有计数器的普通循环。请记住在变量中存储任何可能的长度值,以便不在每个循环中查找长度。