【JavaScript】Event Loop
原文鏈接:《從 JS Event Loop 機制看 Vue 中 nextTick 的實現原理》
Event Loop 即事件循環機制,是理解 JavaScript 運行機制的最關鍵的一點,文章中通過拋出一道題來引入這節課所要講解的內容。
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function executor(resolve) {
console.log(2);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(3);
}).then(function() {
console.log(4);
});
console.log(5);
// result: 2, 3, 5, 4, 1
單線程的 JavaScript
-
定義:所謂單線程,是指在 JS 引擎中負責解釋和執行 JavaScript 代碼的線程只有一個;
-
特點:JS 運行在瀏覽器中,是單線程的,每個 window 一個線程;
-
原因:若爲多線程,在 dom 操作中會產生混亂,如 A 線程修改 dom,B 線程卻刪除了這個 dom;
-
效率:JavaScript 中有很多其他的類線程,也成爲異步事件,如:Ajax請求,監控用戶事件,定時器,讀寫文件等等。
-
過程:當異步事件發生時,將他們放入執行隊列,(主線程)等待當前代碼執行完成。就不會長時間阻塞主線程。等主線程的代碼執行完畢,然後再讀取任務隊列,返回主線程繼續處理。如此循環這就是事件循環機制。
JavaScript 的內存空間
-
棧數據結構
- 結構:
- 特點:先進後出,後進先出(FILO)
-
堆數據結構
- 結構:key - value 結構;
- 特點:存儲的 key - value 是無序的,通過 key 取出,無需關心順序;
-
隊列數據結構
- 結構:
- 特點:先進先出,後進後出(FIFO)
執行上下文 (Execution Context) & 函數調用棧
- 每當控制器轉到可執行代碼的時候,就會進入一個執行上下文;
- 運行環境:
- 全局環境:JavaScript 代碼運行起來會首先進入該環境;
- 函數環境:當函數被調用執行時,會進入當前函數中執行代碼;
- 棧底永遠都是全局上下文,而棧頂就是當前正在執行的上下文;
- 需要注意的是:函數中,遇到 return 關鍵字能直接終止可執行代碼的執行,因此會直接將當期那上下文彈出棧;
- 總結:
- 單線程,依次自頂而下執行,遇到函數就會創建函數執行上下文,併入棧;
- 同步執行,只有棧頂的上下文處於執行中,其他上下文需等待;
- 全局上下文只有一個,它在瀏覽器關閉時出棧;
- 函數執行上下文的個數沒有限制;
- 每次某個函數被調用,就會有個新的執行上下文爲其創建,即使是調用的自身函數,也是如此。
事件循環
-
在 JavaScript 代碼執行的過程中,除了依靠函數調用棧來搞定函數的執行順序外,還依靠任務隊列來搞定另一些代碼的執行;
-
特點:即任務隊列的特點,先進先出;
-
圖例:
-
任務隊列:一個 JS 文件裏事件循環只有一個,但是任務隊列可以有多個,因此任務隊列可以分爲:
-
macro-task (task)
// macro-task (task) 包括: 1. setTimeout / setInterval; 2. setImmediate; 3. I/O operation; 4. UI Rendering
-
micro-task (job)
// micro-task (job) 包括: 1. process.nextTick; 2. Promise; 3. Object.observe (已廢棄); 4. MutationObserver(html5新特性);
-
-
以上這些我們稱他們爲事件源,事件源作爲任務分發器,他們的回調函數纔是被分發到任務隊列,而本身會立即執行,例如:
setTimeout
第一個參數被分發到任務隊列,Promise
的then
方法的回調函數被分發到任務隊列(catch
方法同理); -
不同事件源的事件被分發到不同的任務隊列,其中 setTimeout 和 setInterval 屬於同源;
-
流程:整體代碼開始第一次循環。全局上下文進入函數調用棧。直到調用棧清空(只剩全局),然後執行所有的 job。當所有可執行的 job 執行完畢之後。循環再次從 task 開始,找到其中一個任務隊列執行完畢,然後再執行所有的 job,這樣一直循環下去。
-
規律:task–job–task–job…,往復循環直到沒有可執行代碼。
-
有趣的栗子:
console.log(1);
// 執行到此, Promise 的回調是同步執行,then / catch 纔會被分發到 job 中
new Promise(function(resolve){
console.log(2);
resolve();
}).then(function(){
console.log(3)
})
// 執行到此,setTimeout 執行將回調 function 分發至 task 中
setTimeout(function(){
console.log(4);
process.nextTick(function(){
console.log(5);
})
new Promise(function(resolve){
console.log(6);
resolve()
}).then(function(){
console.log(7)
})
})
// 執行到此, process.nextTick 的回調會被分發到 job 中
process.nextTick(function(){
console.log(8)
})
// 同 setTimeout 原理相同
setImmediate(function(){
console.log(9);
new Promise(function(resolve){
console.log(10);
resolve()
}).then(function(){
console.log(11)
})
process.nextTick(function(){
console.log(12);
})
})
// 最後結果: 1, 2, 8, 3, 4, 6, 5, 7, 9, 10, 12, 11
-
注意:
- 執行遇到了
Promise
,Promise
構造函數的回調函數是同步執行; nextTick
任務隊列會比Promise
的隊列先執行;
- 執行遇到了