-
為什么javascript是單線程的?
因為javascript最初設(shè)計是運行在瀏覽器的,首先系統(tǒng)分配給瀏覽器的內(nèi)存不會很多,其次javascript運行在瀏覽器的目的是為了與用戶進行交互,假設(shè)javascript支持多線程,那么線程A要新增一個節(jié)點,線程B要刪除一個節(jié)點,瀏覽器就不知道到底要干嘛了。
隨著CPU的發(fā)展,HTML5提出了web worker。允許web worker創(chuàng)建子線程,但是子線程得受控于主線程,且不能操作DOM。
Event loop是一個執(zhí)行模型,在不同的地方有不同的實現(xiàn)。在瀏覽器中遵循的是HTML的標準,在node中是基于libuv庫來實現(xiàn)的。
瀏覽器中的Event Loop
HTML標準這樣定義事件循環(huán):
為了協(xié)調(diào)
事件(event),用戶交互(user interaction),腳本(script),渲染(rendering),網(wǎng)絡(luò)(networking)等,用戶代理(user agent)必須使用事件循環(huán)(event loops)。有兩類事件循環(huán):一種針對
瀏覽上下文(browsing context),還有一種針對worker(web worker)。

在主線程執(zhí)行的過程中會產(chǎn)生堆和棧。執(zhí)行棧中的代碼會依次去調(diào)用外部的API。這時候就會分同步代碼和異步代碼。同步方法就去執(zhí)行棧執(zhí)行,調(diào)用到異步方法的時候就會產(chǎn)生任務(wù)隊列,異步方法的回調(diào)就去排隊。執(zhí)行棧中的代碼(同步任務(wù)),總是在讀取"任務(wù)隊列"(異步任務(wù))之前執(zhí)行,在同步代碼執(zhí)行完了,空閑的時候才去看有沒有異步任務(wù)要執(zhí)行,如果這個時候任務(wù)隊列正好有排隊的任務(wù),就去執(zhí)行。
-
任務(wù)隊列分為macro-task和micro-task:
macro-task:setTimeout,setInterval,setImmediate,I/O,UI rendering
micro-task:process.nextTick,Promise,Object.observer,MutationObserver
在執(zhí)行任務(wù)隊列中的方法的時候,會先去執(zhí)行micro-task隊列,再循環(huán)執(zhí)行macro-task隊列。
舉個例子來說,在瀏覽器執(zhí)行以下代碼:
setTimeout(()=>{
console.log(1);
})
console.log(2);
new Promise((resolve,reject)=>{
console.log(3);
resolve();
}).then(()=>{
console.log(4)
});
console.log('end');
這樣一段代碼,執(zhí)行結(jié)果是:2,3,end,4,1
分析一下:
1、在遇到setTimeout的時候,這個方法會去macro-task排隊。
2、顯而易見,同步方法,輸出2。
3、輸出3在resolve()之前,不用等待resolve()執(zhí)行完,所以是同步的,輸出3;輸出4在then()中,需要等待執(zhí)行,屬于Promise,在micro-task排隊。
4、顯而易見,同步方法,輸出end。
5、自此同步代碼已經(jīng)執(zhí)行完了。
6、開始執(zhí)行micro-task隊列,所以輸出4。
9、開始執(zhí)行macro-task隊列,所以輸出1。
node中的Event Loop
nodejs的event loop分為6個階段,每個階段的作用如下:
timers:執(zhí)行
setTimeout()和setInterval()中到期的callback。I/O callbacks:上一輪循環(huán)中有少數(shù)的I/O callback會被延遲到這一輪的這一階段執(zhí)行
idle, prepare:僅內(nèi)部使用(這個存疑,我也不是很懂內(nèi)部在干嘛)
poll:最為重要的階段,執(zhí)行I/O callback,這個指的就是比如去讀數(shù)據(jù)庫啊,讀文件之類的。在適當?shù)臈l件下會阻塞在這個階段(所謂適當?shù)臈l件我要去查一查,現(xiàn)在不確定具體是什么樣的條件叫適當)
check:執(zhí)行setImmediate的callback
close callbacks:執(zhí)行close事件的callback,例如
socket.on("close",func)
當然還是需要注意的就是,在每一個階段執(zhí)行之前還是會先把micro-task里的任務(wù)執(zhí)行掉。
|-----------------micro tasks
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
| | <----- micro tasks
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
| | <----- micro tasks
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘
| | <----- micro tasks
| |
| | ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
| | └───────────────┘
| | <----- micro tasks
│ ┌──────────┴────────────┐
│ │ check │
│ └──────────┬────────────┘
| | <----- micro tasks
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
舉個例子,在node下執(zhí)行代碼:
setTimeout(()=>{
console.log('timer1');
Promise.resolve().then((resolve,reject)=>{
console.log('promise1')
})
})
setTimeout(()=>{
console.log('timer2');
Promise.resolve().then((resolve,reject)=>{
console.log('promise2')
})
})
根據(jù)node下的Event Loop執(zhí)行順序來看,輸出結(jié)果應(yīng)該是:
timer1,timer2,promise1,promise2
分析一下:
首先,micro-task是空的,執(zhí)行timer階段,執(zhí)行setTimeout,輸出timer1,timer2。promise1的then和promise2的then去micro-task排隊。
然后,micro-task隊列里有方法,輸出promise1,promise2
再舉個例子:
setTimeout(()=>{
console.log('timer1');
Promise.resolve().then(()=>{
console.log('promise1')
})
})
setTimeout(()=>{
console.log('timer2');
Promise.resolve().then(()=>{
console.log('promise2')
})
})
Promise.resolve().then(()=>{
console.log('promise3')
})
這段的輸出是:promise3,timer1,timer2,promise1,promise2
分析一下:因為在執(zhí)行timer階段之前,會先把micro-task隊列里的方法執(zhí)行一下,所以promise3雖然寫在后面了,也得先輸出來。
再再舉個栗子:
setTimeout(()=>{
console.log('timer1');
Promise.resolve().then(()=>{
console.log('promise1')
})
})
setTimeout(()=>{
console.log('timer2');
Promise.resolve().then(()=>{
console.log('promise2')
})
})
Promise.resolve().then(()=>{
console.log('promise3')
})
process.nextTick(()=>{
console.log('nexttick')
})
這段的輸出是:nexttick,promise3,timer1,timer2,promise1,promise2
分析一下:因為在micro-task隊列里的優(yōu)先級,process.nextTick一定比promise先執(zhí)行
需要注意的是:是異步方法的回調(diào)在任務(wù)隊列排隊。
/////setTimeout和setImmediate的執(zhí)行順序有待商榷,主要是會涉及到代碼執(zhí)行的時間,和HTML規(guī)定setTimeout默認的4ms問題,不足4ms補足4ms