JavaScript >> Javascript 文檔 >  >> JavaScript

加速你的 JavaScript,第 2 部分

上週,我介紹了 JavaScript 執行時間過長的第一個原因:循環中發生了太多事情。函數也有類似的問題,有時它們做得太多了。通常這意味著有太多的循環(而不是在一個循環中發生的太多)、太多的遞歸,或者只是執行了太多不同的操作。

太多的循環通常是由循環內部的循環導致的,鎖定 JavaScript 引擎直到所有迭代都完成。最明顯的例子是冒泡排序算法。雖然由於原生 sort() 沒有必要在 JavaScript 中使用它 方法,最好了解它是如何產生問題的,這樣您就可以識別相似的模式。 JavaScript 中冒泡排序的典型實現如下所示:

function bubbleSort(items){
    for (var i=items.length-1; i >= 0; i--){
        for (var j=items.length-i; j >= 0; j--){
            if (items[j] < items[j-1]){
                var temp = items[j];
                items[j] = items[j-1];
                items[j-1] = temp;
            }
        }
    }
}

回想你的計算機科學時代,你可能會記得冒泡排序是效率最低的排序算法之一。問題在於每個 n 數組中的項,必須有 *n 2 * 循環迭代。如果有大量數組項,此處理可能需要很長時間。在內循環中進行的比較和交換操作其實很簡單,只是它按順序重複的次數導致了問題。這可能會導致瀏覽器停止運行,並可能導致腳本對話框長時間運行。

幾年前,Yahoo Julien Lecomte 的同事寫了一篇題為
在 Web 瀏覽器中運行 CPU 密集型 JavaScript 計算的文章,其中他描述瞭如何將大型 JavaScript 操作分解為幾個部分。他最清楚的例子之一是將冒泡排序重構為多個步驟,每個步驟執行一次遍歷數組。我對他的代碼做了一些擴充,但方法還是一樣的:

function bubbleSort(array, onComplete){

    var pos = 0;

    (function(){
        var j, value;

        for (j=array.length; j > pos; j--){
            if (array[j] < array[j-1]){
                value = data[j];
                data[j] = data[j-1];
                data[j-1] = value;
            }
        }

        pos++;

        if (pos < array.length){
            setTimeout(arguments.callee,10);
        } else {
            onComplete();
        }
    })();
}

此函數以異步方式執行冒泡排序,每次遍歷數組後停止,然後繼續下一段。 onComplete() 當數組完全排序時調用函數作為數據準備好的通知。 bubbleSort() 函數使用與 chunk() 相同的基本技術 我上一篇文章中介紹的函數:使用匿名函數來包裝行為,然後傳遞 arguments.callee 進入 setTimeout() 重複該過程直到完成。這個函數是一個很好的例子,說明如何將嵌入式循環拆分為一系列步驟以釋放瀏覽器。

類似的問題是遞歸過多。每個額外的遞歸調用都會佔用內存,最終會減慢瀏覽器的速度。令人討厭的是,您可能會在長時間運行的腳本對話框彈出之前達到內存限制並使瀏覽器處於不可用狀態。克羅克福德在他最近的一次演講中對此進行了很好的討論。他使用的例子是一個生成斐波那契數列的函數:

function fibonacci (n) {
    return n < 2 ? n :
            fibonacci(n - 1) +
            fibonacci(n - 2);
};

正如 Crockford 指出的那樣,調用 fibonacci(40) 導致對其自身進行 331,160,280 次調用。避免過多遞歸的解決方案是使用記憶化,一種緩存先前計算的值的技術。 Crockford 引入了以下 memoization 函數,可用於創建處理數字的函數的 memoized 版本:

function memoizer(memo, fundamental) {
    var shell = function (n) {
        var result = memo[n];
        if (typeof result !== 'number') {
            result = fundamental(shell, n);
            memo[n] = result;
        }
        return result;
    };
    return shell;
};

然後他將其應用於斐波那契數列生成器:

var fibonacci =
    memoizer([0, 1], function (recur, n) {
       return recur(n - 1) + recur(n - 2);
    });

調用 fibonacci(40) 使用此代碼只調用了 40 次函數,比原來的代碼有了很大的改進。記憶的整體教訓是,你永遠不應該兩次計算相同的結果。如果您需要不止一次的值,請將其存儲以供以後使用,而不是運行代碼再次生成它。

如前所述,導致函數執行緩慢的最後一件事是它做的太多了。通常是因為這樣的模式:

function doAlot(){
    doSomething();
    doSomethingElse();
    doOneMoreThing();
}

在這裡,正在執行三段明顯不同的代碼。需要注意的重要一點是,沒有一個功能依賴於其他功能來完成它們的任務。它們本質上是相互獨立的,只需要在給定的時間點按順序發生。在這種情況下,您可以使用 chunk() 的變體 不撐起瀏覽器連續執行一系列功能的方法:

function schedule(functions, context){
    setTimeout(function(){
        var process = functions.shift();
        process.call(context);

        if (functions.length > 0){
            setTimeout(arguments.callee, 100);
        }
    }, 100);
}

schedule function 接受兩個參數,一個要執行的函數數組和一個指示 this 值的上下文對象 每個函數內部。 functions 數組充當隊列,每次執行計時器時都會刪除並執行最頂層的函數。該函數可用於連續執行一系列函數,如下所示:

schedule([doSomething, doSomethingElse, doOneMoreThing], window);

我期待 JavaScript 庫很快就會開始包含更多這樣的處理功能。 YUI 在 3.0 版本中已經添加了 Queue 對象,可以幫助管理使用計時器連續運行的多個函數。

不管有什麼工具可以幫助拆分複雜的流程,對於開發人員來說,能夠理解和識別可以從使用這種方法中受益的瓶頸仍然至關重要。無論是有太多循環、太多遞歸,還是發生了太多事情,您現在都知道如何處理每一個。請記住,這裡介紹的技術和功能只是一個起點,而不是金子彈,您應該(並且可能必須)修改提供的代碼,使其適用於您的特定用途。

更新(1/20): 修復了 schedule() 中的複制/粘貼錯誤 功能。

翻譯

  • 中文(簡體)

Tutorial JavaScript 教程
  1. 在 ReactJS 中使用 Husky 和 ​​Lint-Staged 在 git commit 上運行 ESLint

  2. 用 async/await 重寫 MutationObserver()

  3. 使用 MongoDB-Memory-Server 進行測試

  4. 了解 JavaScript 中的引號類型及其用途。

  5. 反應鉤子:useState。組件狀態操作的規則和提示。

  6. 永遠不要再使用數字或貨幣格式庫!

  7. 使用 Puppeteer 實現前端開發自動化。第2部分

  1. 使用拖放生成器克隆 Coinbase 儀表板

  2. CodeSnip:將類型分配給解構的參數

  3. 帶有 IntersectionObserver 的角度延遲加載指令

  4. 用於 JavaScript、Python 和 PHP 的 4.5 小時 Vscode 課程

  5. 帶有簡單 NestJS 服務器的 CI/CD

  6. 我如何在 3 天內製作了一個 Phasmophobia Companion 應用程序

  7. 如何在 TypeScript 中以專業人士的身份輸入 React 道具

  1. 使用 fetch 在 Javascript 中上傳文件

  2. NodeJS 中的線程:使用 Rust 超越事件循環

  3. 如何避免 React 中的默認 Props 渲染陷阱

  4. 回歸基礎:React 中的數據獲取