眾所周知,Javascript是單執行緒語言, 這就意味著,所有的任務都必須按照順序執行,只有等前面的一個任務執行完畢了,下一個任務才能執行。如果前面一個任務耗時很長,後一個任務就得一直等著,因此,為了實現主執行緒的不阻塞,就有了Event Loop。
1、javascript事件迴圈
首先,我們先了解一下同步任務和非同步任務,同步任務指的是,在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;非同步任務指的是,不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有"任務佇列"通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行。
為了更好的瞭解執行機制,看下圖
以上圖說明主執行緒在執行的時候產生堆(記憶體分配)和堆疊(執行上下文),JavaScript是單執行緒的,意味著當執行環境的堆疊中的一個任務(task)在執行的時候,其它的任務都要處於等待狀態。當主程式執行到非同步操作的時候就會將非同步操作對應的task放到event table,指定的事情完成時,Event Table會將這個函式移入Event Queue。主執行緒內的任務執行完畢為空,會去Event Queue讀取對應的函式,進入主執行緒執行,因為這個過程是不斷重複的,所以稱為Event Loop(事件迴圈),接下來,我們用幾個例子進行分析
eg1:
console.log(1);
setTimeout(function () {
console.log(2);
})
console.log(3);
//執行結果:1、3、2
我們來分析一下這段程式碼,首先,根據執行上下文可知,執行環境棧中就有了一個task——console.log(1),輸出1。接著往下執行,因為setTimeout是非同步函式,所以將setTimeout進入event table,註冊了一個回撥函式在event queue,我們暫且稱為fun1,此時的流程如下圖:
接著往下執行,執行環境棧中會建立一個console.log(3)的task,並執行它,輸出3,此時,執行環境已經沒有任務了,則去Event Queue讀取對應的函式,fun1被發現,進入主執行緒輸出2,整個過程已經完成,所以輸出的結果是1、3、2。
eg2:
setTimeout(function () {
console.log(1)
}, 3)
setTimeout(function () {
console.log(2)
})
輸出2,1
我們再來簡單的分析一下這個列子,我們暫且稱第一個setTimeout為Time1,第二個為Time2。由於兩個都是非同步函式,按照執行順序,先將Time放到event Table,接著將Time移到event Table,因為Time在event Table指定要3秒後才執行,所以Time2先於Time1到註冊回撥函式到event queue,所以輸出的結果是2,1。
2、macro-task(巨集任務)、micro-task(微任務)
MacroTask: script(整體程式碼), setTimeout, setInterval, setImmediate(node獨有), I/O, UI rendering
MicroTask: process.nextTick(node獨有), Promises, Object.observe(廢棄), MutationObserver
任務又分為巨集任務和微任務兩種,在同一個上下文中,總的執行順序為“同步程式碼—>microTask—>macroTask”,根據上面event loop的流程圖,我們用列子來做進一步的瞭解:
eg1:
setTimeout(function () {
console.log(1);
},0);
console.log(2);
process.nextTick(() => {
console.log(3);
});
new Promise(function (resolve, rejected) {
console.log(4);
resolve()
}).then(res=>{
console.log(5);
})
setImmediate(function () {
console.log(6)
})
console.log('end');
//輸出2、4、end、3、5、1、6
本例參考《JavaScript中的執行機制》,裡面有詳細的解釋,大家可以參考下。
3、優先順序
我們將上面的例子稍微改一下,將process.nextTick移到promise的後面,看下面的程式碼:
setTimeout(function () {
console.log(1);
},0);
console.log(2);
new Promise(function (resolve, rejected) {
console.log(4);
resolve()
}).then(res=>{
console.log(5);
})
process.nextTick(() => {
console.log(3);
});
setImmediate(function () {
console.log(6)
})
console.log('end');
按照前面的分析,先執行同步程式碼,先輸出“2,4,end”;然後是微任務promise輸出5,process.nextTick輸出3;最後的巨集任務輸出1,6。所以結果為2,4,end,5,3,1,6,然後事實並非如此,結果還是輸出2、4、end、3、5、1、6,這是因為process.nextTick註冊的函式優先順序高於Promise**。
關於Event Loop的其他特殊情況,大家可參考文章一篇文章教會你Event loop——瀏覽器和Node,裡面有更詳細的介紹。