JS的運(yùn)行機(jī)制
先來一個(gè)今日頭條的面試題
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/
1. 單線程的JavaScript
js是單線程的,基于事件循環(huán),非阻塞IO的。
特點(diǎn): 處理I/O型的應(yīng)用,不適合CPU運(yùn)算密集型的應(yīng)用。
說明: 事件循環(huán)中使用一個(gè)事件隊(duì)列,在每個(gè)時(shí)間點(diǎn)上,系統(tǒng)只會處理一個(gè)事件,即使電腦有多個(gè)CPU核心,也無法同時(shí)并行的處理多個(gè)事件。因此,node.js在I/O型的應(yīng)用中,給每一個(gè)輸入輸出定義一個(gè)回調(diào)函數(shù),node.js會自動將其加入到事件輪詢的處理隊(duì)列里,當(dāng)I/O操作完成后,這個(gè)回調(diào)函數(shù)會被觸發(fā),系統(tǒng)會繼續(xù)處理其他的請求。
- 然而單線程不應(yīng)該是自上而下按照順序執(zhí)行的嗎?
- 下面的代碼輸出順序就被打亂了
function fn(){
console.log('start');
setTimeout(()=>{
console.log('setTimeout');
},0);
console.log('end');
}
fn() // 輸出 start end setTimeout
2. JavaScript中的同步異步
js的同步異步是如何實(shí)現(xiàn)的?
js中包含諸多創(chuàng)建異步的函數(shù)如:
seTimeout,setInterval,dom事件,ajax,Promise,process.nextTick等函數(shù)

- 因?yàn)閱尉€程,所以代碼自上而下執(zhí)行,所有代碼被放到
執(zhí)行棧中執(zhí)行; - 遇到異步函數(shù)將回調(diào)函數(shù)添加到一個(gè)
任務(wù)隊(duì)列里面; - 當(dāng)
執(zhí)行棧中的代碼執(zhí)行完以后,會去循環(huán)任務(wù)隊(duì)列里的函數(shù); - 將
任務(wù)隊(duì)列里的函數(shù)放到執(zhí)行棧中執(zhí)行; - 如此往復(fù),稱為
事件循環(huán);
[圖片上傳失敗...(image-5babc5-1559665370997)]
- 這樣分析,上一段的代碼就得到了合理的解釋;
- 再來看一下這段代碼;
function fn() {
setTimeout(()=>{
console.log('a');
},0);
new Promise((resolve)=>{
console.log('b');
resolve();
}).then(()=>{
console.log('c')
});
}
fn() // b c a
Promise和async中的立即執(zhí)行
我們知道Promise中的異步體現(xiàn)在then和catch中,所以寫在Promise中的代碼是被當(dāng)做同步任務(wù)立即執(zhí)行的。而在async/await中,在出現(xiàn)await出現(xiàn)之前,其中的代碼也是立即執(zhí)行的。那么出現(xiàn)了await時(shí)候發(fā)生了什么呢?
await做了什么
從字面意思上看await就是等待,await 等待的是一個(gè)表達(dá)式,這個(gè)表達(dá)式的返回值可以是一個(gè)promise對象也可以是其他值。
很多人以為await會一直等待之后的表達(dá)式執(zhí)行完之后才會繼續(xù)執(zhí)行后面的代碼,實(shí)際上await是一個(gè)讓出線程的標(biāo)志。await后面的表達(dá)式會先執(zhí)行一遍,將await后面的代碼加入到microtask中,然后就會跳出整個(gè)async函數(shù)來執(zhí)行后面的代碼。
由于因?yàn)閍sync await 本身就是promise+generator的語法糖。所以await后面的代碼是microtask。所以對于開始面試題中的
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
等價(jià)于
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
})
}
3. 宏任務(wù)和微任務(wù)
兩任務(wù)在同步異步中處于什么地位?
兩個(gè)任務(wù)分別處于任務(wù)隊(duì)列中的宏隊(duì)列與微隊(duì)列中;
宏隊(duì)列與微隊(duì)列組成了任務(wù)隊(duì)列;
任務(wù)隊(duì)列將任務(wù)放入執(zhí)行棧中執(zhí)行
宏任務(wù):
宏隊(duì)列,macrotask,也叫tasks。
異步任務(wù)的回調(diào)會依次進(jìn)入macro task queue,等待后續(xù)被調(diào)用,
這些異步任務(wù)包括:
- setTimeout
- setInterval
- setImmediate (Node獨(dú)有)
- requestAnimationFrame (瀏覽器獨(dú)有)
- I/O
- UI rendering (瀏覽器獨(dú)有)
微任務(wù):
微隊(duì)列,microtask,也叫jobs。
異步任務(wù)的回調(diào)會依次進(jìn)入micro task queue,等待后續(xù)被調(diào)用,
這些異步任務(wù)包括:
- process.nextTick (Node獨(dú)有)
- Promise
- Object.observe
- MutationObserver
- 執(zhí)行全局Script同步代碼,這些同步代碼有一些是同步語句,有一些是異步語句(比如setTimeout等);
- 全局Script代碼執(zhí)行完畢后,
執(zhí)行棧Stack會清空;- 從
微隊(duì)列中取出位于隊(duì)首的回調(diào)任務(wù),放入執(zhí)行棧Stack中執(zhí)行,執(zhí)行完后微隊(duì)列長度減1;- 繼續(xù)循環(huán)取出位于
微隊(duì)列的任務(wù),放入執(zhí)行棧Stack中執(zhí)行,以此類推,直到直到把微任務(wù)執(zhí)行完畢。注意,如果在執(zhí)行微任務(wù)的過程中,又產(chǎn)生了微任務(wù),那么會加入到微隊(duì)列的末尾,也會在這個(gè)周期被調(diào)用執(zhí)行;微隊(duì)列中的所有微任務(wù)都執(zhí)行完畢,此時(shí)微隊(duì)列為空隊(duì)列,執(zhí)行棧Stack也為空;- 取出
宏隊(duì)列中的任務(wù),放入執(zhí)行棧Stack中執(zhí)行;- 執(zhí)行完畢后,
執(zhí)行棧Stack為空;- 重復(fù)第3-7個(gè)步驟;
以上才是一個(gè)完整的事件循環(huán)
回到面試題
1.首先,事件循環(huán)從宏任務(wù)(macrotask)隊(duì)列開始,這個(gè)時(shí)候,宏任務(wù)隊(duì)列中,只有一個(gè)script(整體代碼)任務(wù);當(dāng)遇到任務(wù)源(task source)時(shí),則會先分發(fā)任務(wù)到對應(yīng)的任務(wù)隊(duì)列中去。
2.然后我們看到首先定義了兩個(gè)async函數(shù),接著往下看,然后遇到了 console 語句,直接輸出 script start。輸出之后,script 任務(wù)繼續(xù)往下執(zhí)行,遇到 setTimeout,其作為一個(gè)宏任務(wù)源,則會先將其任務(wù)分發(fā)到對應(yīng)的隊(duì)列中
3.script 任務(wù)繼續(xù)往下執(zhí)行,執(zhí)行了async1()函數(shù),前面講過async函數(shù)中在await之前的代碼是立即執(zhí)行的,所以會立即輸出async1 start。
遇到了await時(shí),會將await后面的表達(dá)式執(zhí)行一遍,所以就緊接著輸出async2,然后將await后面的代碼也就是console.log('async1 end')加入到microtask中的Promise隊(duì)列中,接著跳出async1函數(shù)來執(zhí)行后面的代碼
4.script任務(wù)繼續(xù)往下執(zhí)行,遇到Promise實(shí)例。由于Promise中的函數(shù)是立即執(zhí)行的,而后續(xù)的 .then 則會被分發(fā)到 microtask 的 Promise 隊(duì)列中去。所以會先輸出 promise1,然后執(zhí)行 resolve,將 promise2 分配到對應(yīng)隊(duì)列
5.script任務(wù)繼續(xù)往下執(zhí)行,最后只有一句輸出了 script end,至此,全局任務(wù)就執(zhí)行完畢了。
根據(jù)上述,每次執(zhí)行完一個(gè)宏任務(wù)之后,會去檢查是否存在 Microtasks;如果有,則執(zhí)行 Microtasks 直至清空 Microtask Queue。
因而在script任務(wù)執(zhí)行完畢之后,開始查找清空微任務(wù)隊(duì)列。此時(shí),微任務(wù)中, Promise 隊(duì)列有的兩個(gè)任務(wù)async1 end和promise2,因此按先后順序輸出 async1 end,promise2。當(dāng)所有的 Microtasks 執(zhí)行完畢之后,表示第一輪的循環(huán)就結(jié)束了
6.第二輪循環(huán)依舊從宏任務(wù)隊(duì)列開始。此時(shí)宏任務(wù)中只有一個(gè) setTimeout,取出直接輸出即可,至此整個(gè)流程結(jié)束
- 來一個(gè)稍微復(fù)雜點(diǎn)的代碼
function fn(){
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3);
});
},0);
new Promise((resolve, reject) => {
console.log(4);
resolve(5);
}).then(data => {
console.log(data);
});
setTimeout(() => {
console.log(6);
},0);
console.log(7);
}
fn(); //
流程重現(xiàn)
- 執(zhí)行函數(shù)同步語句;
-
step1
console.log(1);執(zhí)行棧: [ console ]
宏任務(wù): []
微任務(wù): []打印結(jié)果:
1 -
step2
setTimeout(() => { // 這個(gè)回調(diào)函數(shù)叫做callback1,setTimeout屬于宏任務(wù),所以放到宏隊(duì)列中 console.log(2); Promise.resolve().then(() => { console.log(3) }); });執(zhí)行棧: [ setTimeout ]
宏任務(wù): [ callback1 ]
微任務(wù): []打印結(jié)果:
1 -
step3
new Promise((resolve, reject) => { // 注意,這里是同步執(zhí)行的 console.log(4); resolve(5) }).then((data) => { // 這個(gè)回調(diào)函數(shù)叫做callback2,promise屬于微任務(wù),所以放到微隊(duì)列中 console.log(data); });執(zhí)行棧: [ promise ]
宏任務(wù): [ callback1 ]
微任務(wù): [ callback2 ]打印結(jié)果:
1
4 -
step4
setTimeout(() => { // 這個(gè)回調(diào)函數(shù)叫做callback3,setTimeout屬于宏任務(wù),所以放到宏隊(duì)列中 console.log(6); })執(zhí)行棧: [ setTimeout ]
宏任務(wù): [ callback1 , callback3 ]
微任務(wù): [ callback2 ]打印結(jié)果:
1
4 -
step5
console.log(7)執(zhí)行棧: [ console ]
宏任務(wù): [ callback1 , callback3 ]
微任務(wù): [ callback2 ]打印結(jié)果:
1
4
7
- 同步語句執(zhí)行完畢,從
微隊(duì)列中依次取出任務(wù)執(zhí)行,直到微隊(duì)列為空
-
step6
console.log(data) // 這里data是Promise的成功參數(shù)為5執(zhí)行棧: [ callback2 ]
宏任務(wù): [ callback1 , callback3 ]
微任務(wù): []打印結(jié)果:
1
4
7
5
- 這里
微隊(duì)列中只有一個(gè)任務(wù),執(zhí)行完后開始從宏隊(duì)列中取任務(wù)執(zhí)行
-
step7
console.log(2);執(zhí)行棧: [ callback1 ]
宏任務(wù): [ callback3 ]
微任務(wù): []打印結(jié)果:
1
4
7
5
2但是執(zhí)行
callback1的時(shí)候遇到另一個(gè)Promise,Promise異步執(zhí)行完畢以后在微隊(duì)列中又注冊了一個(gè)callback4函數(shù) -
step8
Promise.resolve().then(() => { // 這個(gè)回調(diào)函數(shù)叫做callback4,promise屬于微任務(wù),所以放到微隊(duì)列中 console.log(3); });執(zhí)行棧: [ Promise ]
宏任務(wù): [ callback3 ]
微任務(wù): [ callback4 ]打印結(jié)果:
1
4
7
5
2
- 取出一個(gè)宏任務(wù)macrotask執(zhí)行完畢,然后再去微任務(wù)隊(duì)列microtask queue中依次取出執(zhí)行
-
step9
console.log(3)執(zhí)行棧: [ callback4 ]
宏任務(wù): [ callback3 ]
微任務(wù): []打印結(jié)果:
1
4
7
5
2
3
-
微隊(duì)列全部執(zhí)行完,再去宏隊(duì)列中取第一個(gè)任務(wù)執(zhí)行
-
step10
console.log(3)執(zhí)行棧: [ callback3 ]
宏任務(wù): []
微任務(wù): []打印結(jié)果:
1
4
7
5
2
3
6
-
以上全部執(zhí)行完畢,
執(zhí)行棧,宏隊(duì)列,微隊(duì)列均為空執(zhí)行棧: []
宏任務(wù): []
微任務(wù): []打印結(jié)果:
1
4
7
5
2
3
6
- 再來一段復(fù)雜代碼
function fn(){
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});
new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
Promise.resolve().then(() => {
console.log(6)
}).then(() => {
console.log(7)
setTimeout(() => {
console.log(8)
}, 0);
});
})
setTimeout(() => {
console.log(9);
})
console.log(10);
}
fn();
4. NodeJS中的事件循環(huán)
NodeJS中的宏任務(wù)和微任務(wù)
NodeJS的
Event Loop中,執(zhí)行宏隊(duì)列的回調(diào)任務(wù)有6個(gè)階段,如下圖:
各個(gè)階段執(zhí)行的任務(wù)如下:
- timers階段:這個(gè)階段執(zhí)行setTimeout和setInterval預(yù)定的callback
-
I/O callback階段:執(zhí)行除了close事件的callbacks、被timers設(shè)定的callbacks、setImmediate()設(shè)定的callbacks這些之外的callbacks
idle, prepare階段:僅node內(nèi)部使用 - poll階段:獲取新的I/O事件,適當(dāng)?shù)臈l件下node將阻塞在這里
- check階段:執(zhí)行setImmediate()設(shè)定的callbacks
- close callbacks階段:執(zhí)行socket.on('close', ....)這些callbacks
NodeJS的宏隊(duì)列:
- Timers Queue
- IO Callbacks Queue
- Check Queue
- Close Callbacks Queue
這4個(gè)都屬于
宏隊(duì)列,但是在瀏覽器中,可以認(rèn)為只有一個(gè)宏隊(duì)列,所有的宏任務(wù)都會被加到這一個(gè)宏隊(duì)列中,但是在NodeJS中,不同的宏任務(wù)會被放置在不同的宏隊(duì)列中
NodeJS的微隊(duì)列:
- Next Tick Queue:是放置process.nextTick(callback)的回調(diào)任務(wù)的
- Other Micro Queue:放置其他
微任務(wù),比如Promise等
在
瀏覽器中,也可以認(rèn)為只有一個(gè)微隊(duì)列,所有的微任務(wù)都會被加到這一個(gè)微隊(duì)列中,但是在NodeJS中,不同的微任務(wù)會被放置在不同的微隊(duì)列中
NodeJS中的事件循環(huán)過程
- 執(zhí)行全局的同步代碼;
- 執(zhí)行
微任務(wù)先執(zhí)行next tick queue所有任務(wù),再執(zhí)行other micro tasks queue中的所有任務(wù); - 開始執(zhí)行
宏任務(wù),共6個(gè)階段,從第1個(gè)階段開始執(zhí)行相應(yīng)每一個(gè)階段宏隊(duì)列中的所有任務(wù),
注意,這里是所有每個(gè)階段宏任務(wù)隊(duì)列的所有任務(wù),在瀏覽器的Event Loop中是只取宏隊(duì)列的第一個(gè)任務(wù)出來執(zhí)行,
每一個(gè)階段的宏任務(wù)執(zhí)行完畢后,開始執(zhí)行微任務(wù),回到步驟2;
Timers Queue-> 步驟2 ->
I/O Queue-> 步驟2 ->
Check Queue-> 步驟2 ->
Close Callback Queue-> 步驟2 ->
Timers Queue
- 再看兩張圖
圖片
- 代碼又來了
function fn(){
console.log('start');
setTimeout(() => { // callback1
console.log(111);
setTimeout(() => { // callback2
console.log(222);
}, 0);
setImmediate(() => { // callback3
console.log(333);
});
process.nextTick(() => { // callback4
console.log(444);
});
}, 0);
setImmediate(() => { // callback5
console.log(555);
process.nextTick(() => { // callback6
console.log(666);
});
});
setTimeout(() => { // callback7
console.log(777);
process.nextTick(() => { // callback8
console.log(888);
});
}, 0);
process.nextTick(() => { // callback9
console.log(999);
});
console.log('end');
}
fn();
// before version 11.0.0 start end 999 111 777 444 888 555 333 666 222
// after version 11.0.0 start end 999 111 444 777 888 555 666 333 222
PS:版本不同導(dǎo)致運(yùn)行結(jié)果不同
總結(jié):
- 瀏覽器的Event Loop和NodeJS的Event Loop是不同的,實(shí)現(xiàn)機(jī)制也不一樣,不要混為一談。
- NodeJS可以理解成有4個(gè)宏任務(wù)隊(duì)列和2個(gè)微任務(wù)隊(duì)列,但是執(zhí)行宏任務(wù)時(shí)有6個(gè)階段。先執(zhí)行全局Script代碼,執(zhí)行完同步代碼調(diào)用棧清空后,先從微任務(wù)隊(duì)列Next Tick Queue中依次取出所有的任務(wù)放入調(diào)用棧中執(zhí)行,再從微任務(wù)隊(duì)列Other Microtask Queue中依次取出所有的任務(wù)放入調(diào)用棧中執(zhí)行。然后開始宏任務(wù)的6個(gè)階段,每個(gè)階段都將該宏任務(wù)隊(duì)列中的所有任務(wù)都取出來執(zhí)行(注意,這里和瀏覽器不一樣,瀏覽器只取一個(gè)),每個(gè)宏任務(wù)階段執(zhí)行完畢后,開始執(zhí)行微任務(wù),再開始執(zhí)行下一階段宏任務(wù),以此構(gòu)成事件循環(huán)。
- MacroTask包括: setTimeout、setInterval、 setImmediate(Node)、requestAnimation(瀏覽器)、IO、UI rendering。
- Microtask包括: process.nextTick(Node)、Promise、Object.observe、MutationObserver。
- v11以前 是上面說的那樣;v11以后將Node環(huán)境的事件循環(huán)和瀏覽器的統(tǒng)一了。
- process.nextTick 上限是1000?
- 寫一個(gè)休眠函數(shù) 達(dá)到阻塞目的
附圖
瀏覽器中的EventLoop

NodeJS中的EventLoop (v11以前,v11以后和瀏覽器一致)
963090bd3b681de3313b4466b234f4f02474.gif
歲月不饒人,我們亦未曾繞過歲月。
贈人玫瑰,手有余香。感謝點(diǎn)贊的你。

