javascript - 我怎样才能改进这个 JavaScript DOM 操作数据结构/算法?

标签 javascript algorithm data-structures

目标

我有一个包含大约 70 个元素的 DOM(包含一些内容的 divs)。我需要大量且快速地移动和切换这些 div 的显示。速度是最重要的事情之一。移动和切换这些 div 的触发器是一个搜索查询,有点像 Google Instant ,除了我移动和切换的所有 DOM 元素都是第一次加载(因此不再调用服务器)。

实现

我通过以下方式实现了这一点:在 DOM 旁边,我传入了一个 JavaScript 对象数组,这些对象表示 div 及其属性,如位置、内容等。这个数组就像是 DOM 的镜像。当用户开始输入时,我开始遍历数组并计算每个 div/对象需要对其执行的操作。我实际上循环了这个数组几次:我首先检查我是否需要查看一个 div/对象,然后我查看对象,然后我是否需要查看内容,然后我查看内容。

我在这些循环中做的一件事是为 DOM 操作设置标志。据我了解,与我正在做的其他事情(循环、读取和写入对象属性等)相比,读取和操作以及 DOM 是 JavaScript 中较慢的操作之一。我也做了一些分析,证实了这个假设。所以在每个 Angular 落我都试图防止“接触”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;
}

最佳答案

评论和格式化 JavaScript,荣誉先生!

首先,您的用例看起来非常适合 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;
}

关于javascript - 我怎样才能改进这个 JavaScript DOM 操作数据结构/算法?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/8444087/

相关文章:

javascript - 需要带参数

java - 修改算法以在包含重复项的字符串中生成唯一排列

c - fread() 从二进制文件读取时出现意外行为

algorithm - 需要想法使用优先级队列在数据结构中自定义算法

javascript - 使用 javascript 知道最接近的选择器后,选择下一个 <div/>

javascript - jQuery .index(),将更改与参数混淆

algorithm - 最小方差生成树的多项式时间算法

c - displayStack函数打印随机字母

javascript - 如何确定我的页面是在普通浏览器还是 SAP CL_GUI_HTML_VIEWER 中呈现

c++ - 使用 C++ 的数组中出现次数最多的元素?