0.從一道題說起
var t = true; setTimeout(function(){ t = false; }, 1000); while(t){ } alert('end');
問,以上代碼何時alert“end”呢?
測試一下:答案是:永遠都不會alert。
解析:JavaScript引擎是單線程的,事件觸發排隊等候。所有任務按照觸發時間先后排隊處理。
上例中,排隊的順序狀態是:
| var t=true ; | while(t){}; | alert(‘end’); |
在1000ms之后,setTimeout函數也加入隊列。
while(t){ }無限循環阻塞了單線程,不管排到后面的代碼執行時間有多短,后面的代碼無法執行,一直阻塞下去。
1.瀏覽器線程
瀏覽器有這么幾大線程:UI渲染線程(用於頁面的渲染),javascript引擎線程(用於處理js),GUI事件觸發線程(用於交互)。
有時會開啟的線程:http傳輸線程,定時觸發線程(定時器)
它們之間的關系是什么呢?
(1)UI渲染線程 與 javascript引擎線程 互斥
由於javascript可以操縱頁面的DOM,所以如果UI渲染線程與javascript引擎線程 不互斥的話,在UI渲染線程進行頁面渲染的同時,javascript引擎線程進行DOM修改,最終會造成DOM狀態不一致的現象。所以,當javascript引擎線程運行的時候,UI渲染線程處於凍結狀態。
(2)javascript引擎線程 與 GUI事件觸發線程(用於交互) 異步
瀏覽器開啟事件觸發線程,等待用戶動作,事件觸發線程解析為響應事件,轉移到javascript引擎線程,排隊等候,等待javascript引擎的處理。
(3)javascript引擎線程 與 http傳輸線程 異步
網頁get,post等請求,xhr異步請求都通過http傳輸線程,傳送到javascript引擎排隊,等候處理。
(4)javascript引擎線程 與 定時觸發線程(定時器) 異步
setTimeout(),setInterval()由單獨的線程 定時觸發線程 觸發,傳送到javascript引擎排隊等候,等待處理。
上述的所有的異步操作有不同的瀏覽器分配線程執行,那個先執行完就先將那個加入到異步隊列中,利用事件的輪詢執行異步隊列中的回調函數
2.xhr異步是障眼法
我們來做一個試驗:
客戶端js代碼
//jquery封裝的ajax請求,請求http://localhost:3000/login頁面 $.ajax({ type: "post", url: "http://localhost:3000/login", dataType: "json", data:{ username: username, password: password }, success: function(data){ if(data){ if(data.message=="i202"){ alert('密碼錯誤,請重新輸入'); window.location.href="login"; }else if(data.message=="i200"){ alert('登陸成功'); window.location.href="index"; } else{ alert('沒有這個用戶名'); window.location.href="login"; } } else{ } } }); //這里有一個無限循環 while(1){}
后端Node.js代碼:
//后台對post的響應 router.post('/login', function (req, res, next) { sleep(1000); res.send({status:"success", message:"i200"}); }); /** * 模擬sleep * @param sleepTime */ function sleep(sleepTime) { for(var start = +new Date; +new Date - start <= sleepTime; ) { } }
前台將永遠不會alert(“登陸成功”)。瀏覽器通過http線程收到xhr響應,但是轉到javascript線程等待執行。javascript單線程,一次只能處理一個任務。第一個任務無限循環,后面的任務全部阻塞。
xhr異步編程實際上是一種障眼法。
3.定時器時間不准
(1)時間不准1
setTimeout(function () { while (true) { } }, 1000); setTimeout(function () { alert('end 2'); }, 2000); setTimeout(function () { alert('end 1'); }, 100); alert('end');
運行這段代碼。運行結果是alert(‘end’) alert(‘end 1’)。
前兩個定時器並不能如約在規定的時間點執行哦。
(2)時間不准2
setTimeout(function(){ /* 代碼塊... */ setTimeout(arguments.callee, 10); }, 10); setInterval(function(){ /*代碼塊... */ }, 10);
兩個定時器,本想實現相同的功能:每十秒觸發一次定時器。
但是實際上,setTimeout在10ms后才加入js執行隊列,排隊等待。所以每兩次定時器觸發的時間間隔可能 > 10ms。
setInterval每10s就向js執行隊列添加一個setInterval事件等待執行。前面的setInterval事件可能被它之前的事件阻塞,導致執行晚了幾拍。那么沒兩次定時時間觸發的時間間隔可能 <10ms 。
4.web worker 才是真正多線程
來吧,試驗一下:
index.html
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> </head> <body> <script src="js/Fthread.js"></script> </body> </html>
Fthread.js
//這里創建一個webworker就是開一個新的線程 var worker=new Worker('js/Sthread.js');//創建子線程 //這里接收新的線程傳來的data worker.onmessage = function(event) { console.log(event.data); }; //這個將會觸發向子進程的請求 worker.postMessage("begin"); //構造一個無限循環 //setTimeout(function () { while (true) { } }, 1000);該定時器不會阻塞線程的交互
Sthread.js
//這里占有一個新線程,向主線程發送消息 postMessage('hello'); //實現之前的一個實例,看是否阻塞 setTimeout(function () { console.log('end 2'); }, 2000); setTimeout(function () { console.log('end 1'); }, 100);
worker.onmessage = function(event) {
console.log(event.data);
}; console.log('end');
運行結果:
end hello //這是兩個線程數據的傳送,可以不看end1 end2
begin end1 end2
hello沒有被定時器異步阻塞,因為人家是在子線程運行滴。這才是多線程嘛。JS中的異步雖然也有開線程進行處理相關的操作,但是其回調函數還是需要通過主線程輪詢執行,因而並不是完全的多線程。
webworker是基於瀏覽器引擎的,為了防止出現像C++之類的的多線程之間的競態、死鎖等現象,Web Worker有如下限制:
- 同源策略限制
- 不能訪問頁面DOM和其他資源
- 瀏覽器實現程度不同
同時web worker線程與主線程不共享作用域(只是進行簡單消息傳遞而非數據的引用),這也會有問題當兩者在進行消息通訊時會將同一個變量賦值兩次,導致內存的占用增大,可以通過對大的數據集利用Transferable對象,進行所有權的傳遞,當數據傳遞出去后,原來的線程中將對該數據集失去訪問的權限,這樣可以避免作用域共享帶來的問題,當然,所有權的傳遞是雙向的。
我們可以在worker內部做這些事情:
- 可以執行網絡操作(Ajax、Web Sockets)
- 可以使用定時器(set/clearTimeout()、set/clearInterval())
- 訪問某些重要全局變量及功能的復本(navigator、location、JSON、applicationCache)
- 可以使用importScrips()加載額外js腳本
Web worker有兩種形式一個是共享web worker(多對一,通過端口號進行區分),另一個是專用 web worker(實現一對一),上面的那個例子就是專用web worker的例子。
下面舉一個共享web worker 的例子:
1、創建共享worker
var worker = new SharedWorker('scripts/worker.js');
2、每個主程序利用端口號區分(多個主程序的初始化是一樣的,只是在初始化時分配的端口不同)
//demo.js var worker = new SharedWorker('scripts/worker.js'); worker.port.addEventListener('message', function(e){ console.log(e.data); //500000000067109000 }, false); worker.port.start();啟動web worker,初始化端口
3、worker線程中的內容
//worker.js
function calculate(){
var ret = 0;
for(var i = 1; i <= 1e9; i++){
ret += i;
}
return ret;
}
addEventListener('connect', function(e){
var port = e.ports[0];//
用於獲取連接分配的端口,區分主線程
port.start();
port.postMessage(calculate());
});
4、其他的使用如終止web worker有兩種方式,使用和專用線程是一樣的
//主線程 worker.terminate();
//工作線程 close();