(轉)優化js腳本設計,防止瀏覽器假死

    在Web開發的時候常常會遇到瀏覽器不響應事件進入假死狀態,甚至彈出「腳本運行時間過長「的提示框,若是出現這種狀況說明你的腳本已經失控了,必須進行優化。javascript

爲何會出現這種狀況呢,咱們先來看一下瀏覽器的內核處理方式:java

 
    瀏覽器的內核是多線程的,它們在內核制控下相互配合以保持同步,一個瀏覽器至少實現三個常駐線程:javascript引擎線程,GUI渲染線程,瀏覽器事件觸發線程。
 
  1. JavaScript引擎是基於事件驅動單線程執行的,JS引擎一直等待着任務隊列中任務的到來而後加以處理,瀏覽器不管再何時都只有一個JS線程在運行JS程序。
  2. GUI 渲染線程負責渲染瀏覽器界面,當界面須要重繪(Repaint)或因爲某種操做引起迴流(reflow)時,該線程就會執行。但須要注意 GUI渲染線程與JS引擎是互斥的,當JS引擎執行時GUI線程會被掛起,GUI更新會被保存在一個隊列中等到JS引擎空閒時當即被執行。
  3. 事件觸發線程,當一個事件被觸發時該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理。這些事件可來自JavaScript引擎當前執行的代碼塊如setTimeOut、也可來自瀏覽器內核的其餘線程如鼠標點擊、AJAX異步請求等,但因爲JS的單線程關係全部這些事件都得排隊等待JS引擎處理。
 

瞭解了瀏覽器的內核處理方式就不難理解瀏覽器爲何會進入假死狀態了,當一段JS腳本長時間佔用着處理機就會掛起瀏覽器的GUI更新,然後面的事件響應也被排在隊列中得不處處理,從而形成了瀏覽器被鎖定進入假死狀態。另外JS腳本中進行了DOM操做,一旦JS調用結束就會立刻進行一次GUI渲染,而後纔開始執行下一個任務,因此JS中大量的DOM操做也會致使事件響應緩慢甚至真正卡死瀏覽器,如在IE6下一次插入大量的HTML。而若是真的彈出了「腳本運行時間過長「的提示框則說明你的JS腳本確定有死循環或者進行過深的遞歸操做了。算法

Nicholas C. Zakas認爲不論什麼腳本,在任什麼時候間、任何瀏覽器上執行都不該該超過100毫秒,不然必定要將腳本分解成若干更小的代碼段。那麼咱們該如何來作呢:shell

第一步,優化你的循環,循環體中包含太多的操做和循環的次數過多都會致使循環執行時間過長,並直接致使鎖死瀏覽器。若是循環以後沒有其餘操做,每次循環只處理一個數值,並且不依賴於上一次循環的結果則能夠對循環進行拆解,看下面的chunk的函數:數組

 

function chunk(array, process, context) {瀏覽器

    setTimeout(function() {緩存

    var item = array.shift();多線程

    process.call(context, item);閉包

    if (array.length > 0) {異步

        setTimeout(arguments.callee, 100);

    }), 100);

}

 

chunk()函數的用途就是將一個數組分紅小塊處理,它接受三個參數:要處理的數組,處理函數以及可選的上下文環境。每次函數都會將數組中第一個對象取出交給process函數處理,若是數組中還有對象沒有被處理則啓動下一個timer,直到數組處理完。這樣可保證腳本不會長時間佔用處理機,使瀏覽器出一個高響應的流暢狀態。

其實在我看來,藉助JS強大的閉包機制任何循環都是可拆分的,下面的版本增長了callback機制,使可再循環處理完畢以後進行其餘的操做。

 

function chunk(array,process,cbfun){

    var i=0,len = array.length;    //這裏要注意在執行過程當中數組最好是不變的

    setTimeout(function(){

        process( array[i] , i++ );    //循環體要作的操做

        if( i < len ){

            setTimeout(arguments.callee,100);

        }else{

            cbfun()                //循環結束以後要作的操做

        }

    }

}

 

第二步,優化你的函數,若是函數體內有太多不相干但又要一塊兒執行的操做則能夠進行拆分,考慮下面的函數:

 

function dosomething(){

    dosomething1();

    dosomething2();

}

 

dosomething1和dosomething2互不相干,執行沒有前後次序,可用前面提到的chunk函數進行拆分:

 

function dosomething(){

    chunk([dosomething1,dosomething2],function(item){item();})

}

 

或者直接交給瀏覽器去調度

function dosome(){

    setTimeout(dosomething1,0);

    setTimeout(dosomething2,0);

}

 

第三步,優化遞歸操做,函數遞歸雖然簡單直接可是過深的遞歸操做不但影響性能並且稍不注意就會致使瀏覽器彈出腳本失控對話框,必須當心處理。

看如下斐波那契數列的遞歸算法:

function fibonacci(n) {

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

};

 

fibonacci(40)這條語句將重複調用自身331160280次,在瀏覽器中執行必然致使腳本失控,而採用下面的算法則只須要調用40次

fibonacci = function(n){

    var memo = {0:0,1:0};         //計算結果緩存

    var shell = function(n){

        var result = memo[n];

        iftypeof result != 'number' )    //若是值沒有被計算則進行計算

            memo[n] = shell(n-1) + shell(n -2);

            return memo[n];

        }

        return shell(n);

}

 

這項技術被稱爲memoization,他的原理很簡單就是一樣的結果你不必計算兩次。另外一種消除遞歸的辦法就是利用迭代,遞歸和迭代常常會被做爲互相彌補的方法。

第四步,減小DOM操做,DOM操做的代價是至關昂貴的,大多數DOM操做都會觸發瀏覽器的迴流(reflow)操做。例如添加刪除節點,修改元素樣式,獲取須要通過計算的元素樣式等。咱們要作的就是儘可能少的觸發迴流操做。

 

el.style.width = '300px' el.style.height = '300px'

el.style.backgroundColor = 'red'

 

上面的操做會觸發瀏覽器的三次迴流操做,再看下面的方式:

 

el.className = 'newStyle'

 

經過設置改元素的className一次設置多個樣式屬性,將樣式寫再CSS文件中,只觸發一次迴流,達到了一樣是效果並且效率更高。由於瀏覽器最擅長的就是根據class設置樣式。

還有不少能夠減小DOM操做的方法,在此就很少說了,可是一個基本的原則就是讓瀏覽器去作它本身擅長的事情,例如經過class來改變元素的屬性。

相信通過上面的優化的過程一定能夠大大提升用戶體驗,不會出現瀏覽器被鎖死和彈出腳本失控的對話框,使你的瀏覽器從繁重的任務中解放出來。須要指出的是上面這些優化並非必須的,只有當一段腳本的執行時間真的影響到了用戶體驗才須要進行。雖然它們讓用戶以爲腳本的執行變快了,但其實完成同一個操做的時間可能被延長了,這些技術只是讓瀏覽器處於一個快速響應的狀態,使用戶瀏覽更流暢。

 

最後送一句忠告:過早優化是萬惡之源。

相關文章
相關標籤/搜索