先看一段代碼:
setTimeout(function(){
alert("a");
}, 0);
while(1);
alert("b");
希望在馬山可以彈出一個警告提示框“a”來,但是始終沒有來;而且,在FireFox中跑還得到了這樣的提示,並提示你是否要終止這段指令碼的執行,遇事我選擇終止以後,“a”倒是彈出來了,但是“b”卻彈不出來了:
Warning: Unresponsive Script
A script on this page may be busy, or it may have stopped responding. You can stop the script now, or you can continue to see if the script will complete.
因為瀏覽器多個事件放入隊列中執行,每個事件執行的過程當中,是沒法中斷的(比如有滑鼠響應事件、頁面渲染事件、還有setTimeout定義的事件等等)。“b”所在的那段指令碼被終止了,但是“a”所在的那段邏輯已經進入了事件隊列,並沒有被終止。從這個例子也可以看出,JavaScript的順延強制並不準確。但是話說回來,既然這裡希望馬上執行,為什麼要使用setTimeout方法呢?
原因很簡單,因為這裡我希望把這個彈框的邏輯放到事件隊列中去。
為什麼要設計成單線程的
其實javascript核心語言沒有包含任何線程機制的,還有用戶端的javascript也是沒有明確定義線程機制,但是javascript還是嚴格按照”單線程”的模型去執行代碼。為什嗎?網上很多聲音都說這和它的曆史有關係,但是,其實有一個更重要的原因——死結。多線程的GUI架構特別容易死結,這篇文章《Multithreaded toolkits: A failed dream?》描述了其中的緣由,大致是說GUI的行為大多都是從更抽象的頂部一層一層調用到作業系統層級,而事件則是反過來,從下網上冒泡,結果就是兩個方向相反的行為在碰頭,給資源加鎖的時候一個正序,一個逆序,極其容易出現互相等待而餓死的情況,而這種情況下要解決這一問題無異於“fight back an oceanic tidal force”——推薦閱讀。AWT最初其實就是想設計成多線程的,但是使用者非常容易引起死結和競爭,最後Swing還是做成了單線程的。但凡這種event loop+單線程執行的模式,我們還可以找到很多,比如JDK的GUI執行緒模式,主線程就是一個“主事件迴圈”(再後來才引入了Event Dispatch Thread,但這並不改變整體的基本執行緒模式),還有Mac系統的Cocoa等等,都是這樣的模式。
另外,關於thread還是event,這兩種典型模式的優劣比較,在《The Case of Threads vs. Events》這篇文章中有詳細的比較:
偽sleep方法
JavaScript是沒有sleep方法的,正因為它是單線程執行的,sleep方法是沒有意義的。如果非要sleep,我們只能實現一個沒有意義的偽sleep:
function sleep(time) {
var start = new Date().getTime();
while (true) {
if (new Date().getTime() - start > time) {
break;
}
}
}
但是這個偽sleep是沒有意義的,因為這個迴圈會不斷消耗CPU去比對時間(要不消耗CPU去比對時間是需要系統調用的,這在JavaScript裡面是不可能實現的),並不是真正的sleep,而是沒有響應地工作。
拆分耗時邏輯
很多時候我們需要把耗時的邏輯拆分,騰出時間來給其他邏輯的執行:下面的代碼源自《Timed array processing in JavaScript》這篇文章,作者首先給出一個這樣的拆分邏輯執行的架構代碼:
function chunk(array, process, context){
var items = array.concat(); //clone the array
setTimeout(function(){
var item = items.shift();
process.call(context, item);
if (items.length > 0){
setTimeout(arguments.callee, 100);
}
}, 100);
}
但他同時也馬上指出了其中的問題,100毫秒的間隔延時太長了,也許25毫秒就夠了,但是不能為0,0也可以使得這個執行拆分成多個事件進入隊列,但是我們需要給UI的更新渲染等等留一些時間。於是他又改進了一下:
//Copyright 2009 Nicholas C. Zakas. All rights reserved.
//MIT Licensed
function timedChunk(items, process, context, callback){
var todo = items.concat(); //create a clone of the original
setTimeout(function(){
var start = +new Date();
do {
process.call(context, todo.shift());
} while (todo.length > 0 && (+new Date() - start < 50));
if (todo.length > 0){
setTimeout(arguments.callee, 25);
} else {
callback(items);
}
}, 25);
}
可以看見,這可以更充分地利用時間,執行的任務放到一個數組中,只要每次chunk內執行的時間不足50毫秒,就繼續執行;一旦超過50毫秒,就留給外來事件25毫秒去處理。
Web Worker
本質上說,web worker 是運行在背景 JavaScript,不會影響頁面的效能。 當在 HTML 頁面中執行指令碼時,頁面的狀態是不可響應的,直到指令碼已完成。Web worker 是運行在背景 JavaScript,獨立於其他指令碼,不會影響頁面的效能。這可以看做是HTML5嘗試為單線程JavaScript弊端做的改進(當前問題還有不少,譬如瀏覽器差異)。
var w=new Worker("w.js");
和web worker通訊,需要在worker的w.js中實現一個onmessage函數:
onmessage =function (evt){
var data = evt.data;
postMessage(data); // 發訊息給主線程
}
再在主線程中:
worker.postMessage("hello world"); //發訊息給背景工作執行緒
w.onmessage = function (event) {
// ...
}
把嵌套調用變成鏈式調用
這和今天的話題有點遠,但是因為setTimeout使用得多了,這個問題幾乎必現,所以在此提一提。這個問題就是嵌套層次過深的問題,這在設計JavaScript架構的時候尤其明顯:
setTimeout(function(){
// logic
setTimeout(function(){
// logic
setTimeout(function(){
// logic
}, 200);
}, 20);
}, 100);
有一個通用的最佳化辦法,把嵌套調用最佳化成鏈式調用:
runner.push(function() {
// logic
}, 100)
.push(function() {
// logic
}, 20)
.push(function() {
// logic
}, 200)
.run();