這篇是Nicholas讨論如果防止腳本失控的第二篇,主要讨論了如何重構嵌套循環、遞歸,以及那些在函數内部同時執行很多子操作的函數。基本的思
想和上一節trunk()那個例子一緻,如果幾個操作沒有特定的執行順序,而且互相不是依賴關系,我們就可以通過異步調用的方式加以執行,不止可以減少執
行的次數,還可以防止腳本失控。本文還介紹了通過memoization技術取代遞歸的方法。
【原文标題】Speed up your JavaScript, Part 2
【原文作者】Nicholas C. Zakas
以下是對原文的翻譯:
上周我在《too much happening in a loop》(譯文:如何提升JavaScript的運作速度(循環篇))這篇文章中介紹了JavaScript運作時間過長的第一個原因。相似的情況有時也出現在函數的定義上,函數也可能因為使用不當而過載使用。通常情況是函數内包含了過多的循環(不是在循環中執行了過多的内容),太多的遞歸,或者隻不過是太多不相幹但又要一起執行的操作。
太
多的循環經常是以嵌套的形式出現,這種代碼會一直占用JavaScript引擎直至循環結束。這方面有一個非常著名的例子,就是使用冒泡算法排序。由于
JavaScript有内置的sort()方法,我們沒有必要使用這種方式進行排序,但我們可以借助這個算法了解嵌套循環占用資源的症結所在,進而避免類
似情況的發生。下面是一個在JavaScript使用冒泡排序法的典型例子:
回憶一下你在學校學習的計算機知識,你可能記得冒泡排序法是效率最低的排序算法之一,原因是對于一個包含n個元素的數組,必須要進行n的平方次的循環操
作。如果數組中的元素數非常大,那麼這個操作會持續很長時間。内循環的操作很簡單,隻是負責比較和交換數值,導緻問題的最大原因在于循環執行的次數。這會
導緻浏覽器運作異常,潛在的直接結果就是那個腳本失控的警告對話框。
幾年前,Yahoo的研究員Julien Lecomte寫了一篇題為《Running CPU Intensive JavaScript Computations in a Web Browser》的文章,在這篇文章中作者闡述了如何将很大的javaScript操作分解成若幹小部分。其中一個例子就是将冒泡排序法分解成多個步驟,每個步驟隻周遊一次數組。我對他的代碼做了改進,但方法的思路還是一樣的:
這個函數借助一個異步管理器來實作了冒泡算法,在每次周遊數組以前暫停一下。onComplete()函數會在數組排序完成後觸發,提示使用者資料已經準備
好。bubbleSort()函數使用了和chunk()函數一樣的基本技術(參考我的上一篇文章),将行為包裝在一個匿名函數中,将
arguments.callee傳遞給setTimeout()以達到重複操作的目的,直至排序完成。如果你要将嵌套的循環拆解成若幹個小步驟,以達到
解放浏覽器的目的,這個函數提供了不錯的指導意見。
相似的問題還包括過多的遞歸。每個額外的遞歸調用都會占用更多的記憶體,進而減慢浏覽器的運作。惱人的是,你可能在浏覽器發出腳本失控警告之前,就耗盡了系統的記憶體,導緻浏覽器處于停止響應的狀态。Crockford在部落格上曾經對這個問題進行過深入的讨論。他當時使用的例子,就是用遞歸生成一個斐波那契數列。
按照Crockford的說法,執行fibonacci(40)這條語句将重複調用自身331160280次。避免使用遞歸的方案之一就是使用memoization技術,這項技術可以擷取上一次調用的執行結果。Crockford介紹了下面這個函數,可以為處理數值的函數增加這項功能:
他接下來将這個函數應用在斐波那契數列生成器上:
這時如果我們再次調用fibonacci(40),隻會重複調用40次,和原來相比提高得非常多。memoization的原理,概括起來就一句話,同樣的結果,你沒有必要計算兩次。如果一個結果你可能會再次使用,把這個結果儲存起來,總比重新計算一次來的快。
最後一個可能讓函數執行緩慢的原因,就是我們之前提到過的,函數裡面執行了太多的内容,通常是因為使用了類似下面的開發模式:
在這裡要執行三個不同的函數,請注意,無論是哪個函數,在執行過程中都不依賴其他的函數,他們在本質是相對獨立的,隻是需要在一個特定時間逐一執行而已。同樣,你可以使用類似chunk()的方法來執行一系列函數,而不會導緻鎖定浏覽器。
schedule函數有兩個參數,一個是包含要執行函數的數組,另外一個是标明this所屬的上下文對象。函數數組以隊列方式實作,Timer事件每次觸發的時候,都會将隊列最前面的函數取出并執行,這個函數可以通過下面的方式執行一系列函數:
很希望各個JavaScript的類庫都增加類似這樣的程序處理函數。YUI在3.0時就已經引入了Queue對象,可以通過timer連續調用一組函數。
無
論現有的技術可以幫助我們将複雜的程序拆分到什麼程度,對于開發者來說,使用這種方法來了解并确定腳本失控的瓶頸是非常重要的。無論是太多的循環、遞歸還
是其他的什麼,你現在應該知道如果處理類似的情況。但要記住,這裡提到的技術和函數隻是起到抛磚引玉的作用,在實際的應用中,你應該對它們加以改進,這樣
才能發揮更大的作用。