這篇是Nicholas討論若是防止腳本失控的第二篇,主要討論瞭如何重構嵌套循環、遞歸,以及那些在函數內部同時執行不少子操做的函數。基本的思 想和上一節trunk()那個例子一致,若是幾個操做沒有特定的執行順序,並且互相不是依賴關係,咱們就能夠經過異步調用的方式加以執行,不止能夠減小執 行的次數,還能夠防止腳本失控。本文還介紹了經過memoization技術取代遞歸的方法。javascript
【原文標題】Speed up your JavaScript, Part 2
【原文做者】Nicholas C. Zakas
如下是對原文的翻譯:
上週我在《too much happening in a loop》(譯文:如何提高JavaScript的運行速度(循環篇))這篇文章中介紹了JavaScript運行時間過長的第一個緣由。類似的狀況有時也出如今函數的定義上,函數也可能由於使用不當而過載使用。一般狀況是函數內包含了過多的循環(不是在循環中執行了過多的內容),太多的遞歸,或者只不過是太多不相干但又要一塊兒執行的操做。
太 多的循環常常是以嵌套的形式出現,這種代碼會一直佔用JavaScript引擎直至循環結束。這方面有一個很是著名的例子,就是使用冒泡算法排序。因爲 JavaScript有內置的sort()方法,咱們沒有必要使用這種方式進行排序,但咱們能夠藉助這個算法理解嵌套循環佔用資源的癥結所在,從而避免類 似狀況的發生。下面是一個在JavaScript使用冒泡排序法的典型例子:
html
function bubbleSort(items) {
for (var i = items.length - 1; i >= 0; i--) {
for (var j = 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的平方次的循環操 做。若是數組中的元素數很是大,那麼這個操做會持續很長時間。內循環的操做很簡單,只是負責比較和交換數值,致使問題的最大緣由在於循環執行的次數。這會 致使瀏覽器運行異常,潛在的直接結果就是那個腳本失控的警告對話框。
幾年前,Yahoo的研究員Julien Lecomte寫了一篇題爲《Running CPU Intensive JavaScript Computations in a Web Browser》的文章,在這篇文章中做者闡述瞭如何將很大的javaScript操做分解成若干小部分。其中一個例子就是將冒泡排序法分解成多個步驟,每一個步驟只遍歷一次數組。我對他的代碼作了改進,但方法的思路仍是同樣的:
java
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()以達到重複操做的目的,直至排序完成。若是你要將嵌套的循環拆解成若干個小步驟,以達到 解放瀏覽器的目的,這個函數提供了不錯的指導意見。
類似的問題還包括過多的遞歸。每一個額外的遞歸調用都會佔用更多的內存,從而減慢瀏覽器的運行。惱人的是,你可能在瀏覽器發出腳本失控警告以前,就耗盡了系統的內存,致使瀏覽器處於中止響應的狀態。Crockford在博客上曾經對這個問題進行過深刻的討論。他當時使用的例子,就是用遞歸生成一個斐波那契數列。
web
function fibonacci(n) {
return n < 2 ? n: fibonacci(n - 1) + fibonacci(n - 2);
};
按照Crockford的說法,執行fibonacci(40)這條語句將重複調用自身331160280次。避免使用遞歸的方案之一就是使用memoization技術,這項技術能夠獲取上一次調用的執行結果。Crockford介紹了下面這個函數,能夠爲處理數值的函數增長這項功能:
算法
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;
};
他接下來將這個函數應用在斐波那契數列生成器上:
shell
var fibonacci = memoizer([0, 1],
function(recur, n) {
return recur(n - 1) + recur(n - 2);
});
這時若是咱們再次調用fibonacci(40),只會重複調用40次,和原來相比提升得很是多。memoization的原理,歸納起來就一句話,一樣的結果,你沒有必要計算兩次。若是一個結果你可能會再次使用,把這個結果保存起來,總比從新計算一次來的快。
最後一個可能讓函數執行緩慢的緣由,就是咱們以前提到過的,函數裏面執行了太多的內容,一般是由於使用了相似下面的開發模式:
數組
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函數有兩個參數,一個是包含要執行函數的數組,另一個是標明this所屬的上下文對象。函數數組以隊列方式實現,Timer事件每次觸發的時候,都會將隊列最前面的函數取出並執行,這個函數能夠經過下面的方式執行一系列函數:
app
schedule([doSomething, doSomethingElse, doOneMoreThing], window);
很但願各個JavaScript的類庫都增長相似這樣的進程處理函數。YUI在3.0時就已經引入了Queue對象,能夠經過timer連續調用一組函數。
無 論現有的技術能夠幫助咱們將複雜的進程拆分到什麼程度,對於開發者來講,使用這種方法來理解並肯定腳本失控的瓶頸是很是重要的。不管是太多的循環、遞歸還 是其餘的什麼,你如今應該知道若是處理相似的狀況。但要記住,這裏提到的技術和函數只是起到拋磚引玉的做用,在實際的應用中,你應該對它們加以改進,這樣 才能發揮更大的做用。異步