在Web開發的時候經常會遇到瀏覽器不響應事件進入假死狀態,甚至彈出“腳本運行時間過長“的提示框,如果出現這種情況說明你的腳本已經失控了,必須進行優化。
為什么會出現這種情況呢,我們先來看一下瀏覽器的內核處理方式:
瀏覽器的內核是多線程的,它們在內核制控下相互配合以保持同步,一個瀏覽器至少實現三個常駐線程:javascript引擎線程,GUI渲染線程,瀏覽器事件觸發線程。
JavaScript引擎是基於事件驅動單線程執行的,JS引擎一直等待着任務隊列中任務的到來然后加以處理,瀏覽器無論再什么時候都只有一個JS線程在運行JS程序。
GUI 渲染線程負責渲染瀏覽器界面,當界面需要重繪(Repaint)或由於某種操作引發回流(reflow)時,該線程就會執行。但需要注意 GUI渲染線程與JS引擎是互斥的,當JS引擎執行時GUI線程會被掛起,GUI更新會被保存在一個隊列中等到JS引擎空閑時立即被執行。
事件觸發線程,當一個事件被觸發時該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理。這些事件可來自JavaScript引擎當前執行的代碼塊如setTimeOut、也可來自瀏覽器內核的其他線程如鼠標點擊、AJAX異步請求等,但由於JS的單線程關系所有這些事件都得排隊等待JS引擎處理。
了解了瀏覽器的內核處理方式就不難理解瀏覽器為什么會進入假死狀態了,當一段JS腳本長時間占用着處理機就會掛起瀏覽器的GUI更新,而后面的事件響應也被排在隊列中得不到處理,從而造成了瀏覽器被鎖定進入假死狀態。另外JS腳本中進行了DOM操作,一旦JS調用結束就會馬上進行一次GUI渲染,然后才開始執行下一個任務,所以JS中大量的DOM操作也會導致事件響應緩慢甚至真正卡死瀏覽器,如在IE6下一次插入大量的HTML。而如果真的彈出了“腳本運行時間過長“的提示框則說明你的JS腳本肯定有死循環或者進行過深的遞歸操作了。
Nicholas C. Zakas認為不論什么腳本,在任何時間、任何瀏覽器上執行都不應該超過100毫秒,否則一定要將腳本分解成若干更小的代碼段。那么我們該如何來做呢:
第一步,優化你的循環,循環體中包含太多的操作和循環的次數過多都會導致循環執行時間過長,並直接導致鎖死瀏覽器。如果循環之后沒有其他操作,每次循環只處理一個數值,而且不依賴於上一次循環的結果則可以對循環進行拆解,看下面的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(); //循環結束之后要做的操作
}
}, 100);
}
第二步,優化你的函數,如果函數體內有太多不相干但又要一起執行的操作則可以進行拆分,考慮下面的函數:
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];
if( typeof 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來改變元素的屬性。
相信經過上面的優化的過程必定可以大大提高用戶體驗,不會出現瀏覽器被鎖死和彈出腳本失控的對話框,使你的瀏覽器從繁重的任務中解放出來。需要指出的是上面這些優化並不是必須的,只有當一段腳本的執行時間真的影響到了用戶體驗才需要進行。雖然它們讓用戶覺得腳本的執行變快了,但其實完成同一個操作的時間可能被延長了,這些技術只是讓瀏覽器處於一個快速響應的狀態,使用戶瀏覽更流暢。