Event Loop
前言
Event Loop即事件迴圈,是指瀏覽器或Node的一種解決javaScript單執行緒執行時不會阻塞的一種機制,也就是我們經常使用非同步的原理。
為啥要弄懂Event Loop
是要增加自己技術的深度,也就是懂得JavaScript的執行機制。
現在在前端領域各種技術層出不窮,掌握底層原理,可以讓自己以不變,應萬變。
應對各大網際網路公司的面試,懂其原理,題目任其發揮。
堆,棧、佇列

堆(Heap)
堆 是一種資料結構,是利用完全二叉樹維護的一組資料, 堆 分為兩種,一種為最大 堆 ,一種為 最小堆 ,將根節點 最大 的 堆 叫做 最大堆 或 大根堆 ,根節點 最小 的 堆 叫做 最小堆 或 小根堆 。
堆 是 線性資料結構 ,相當於 一維陣列 ,有唯一後繼。
如最大堆

棧(Stack)(後進先出)
棧 在電腦科學中是限定僅在 表尾 進行 插入 或 刪除 操作的線性表。 棧 是一種資料結構,它按照 後進先出 的原則儲存資料, 先進入 的資料被壓入 棧底 , 最後的資料 在 棧頂 ,需要讀資料的時候從 棧頂 開始 彈出資料 。
棧 是隻能在 某一端插入 和 刪除 的 特殊線性表 。

佇列(Queue)(先進先出)
特殊之處在於它只允許在表的前端(front)進行 刪除 操作,而在表的後端(rear)進行 插入 操作,和 棧 一樣, 佇列 是一種操作受限制的線性表。
進行 插入 操作的端稱為 隊尾 ,進行 刪除 操作的端稱為 隊頭 。 佇列中沒有元素時,稱為 空佇列 。
佇列 的資料元素又稱為 佇列元素 。在佇列中插入一個佇列元素稱為 入隊 ,從 佇列 中 刪除 一個佇列元素稱為 出隊 。因為佇列 只允許 在一端 插入 ,在另一端 刪除 ,所以只有 最早 進入 佇列 的元素 才能最先從佇列中 刪除,故佇列又稱為 先進先出 (FIFO—first in first out)

Event Loop
在JavaScript中,任務被分為兩種,一種 巨集任務 (MacroTask)也叫Task,一種叫 微任務 (MicroTask)。
MacroTask(巨集任務)
script全部程式碼、setTimeout、setInterval、setImmediate(瀏覽器暫時不支援,只有IE10支援,具體可見 MDN )、I/O、UI Rendering。
MicroTask(微任務)
Process.nextTick(Node獨有)、Promise、Object.observe(廢棄)、MutationObserver(具體使用方式檢視 這裡 )
瀏覽器中的Event Loop
Javascript 有一個 main thread 主執行緒和 call-stack 呼叫棧(執行棧),所有的任務都會被放到呼叫棧等待主執行緒執行。
JS呼叫棧
JS呼叫棧採用的是後進先出的規則,當函式執行的時候,會被新增到棧的頂部,當執行棧執行完成後,就會從棧頂移出,直到棧內被清空。
同步任務和非同步任務
Javascript單執行緒任務被分為 同步任務 和 非同步任務 ,同步任務會在呼叫棧中按照順序等待主執行緒依次執行,非同步任務會在非同步任務有了結果後,將註冊的回撥函式放入任務佇列中等待主執行緒空閒的時候(呼叫棧被清空),被讀取到棧內等待主執行緒的執行。

任務佇列Task Queue,即佇列,是一種先進先出的一種資料結構。

事件迴圈的程序模型
選擇當前要執行的任務佇列,選擇任務佇列中最先進入的任務,如果任務佇列為空即null,則執行跳轉到微任務(MicroTask)的執行步驟。
將事件迴圈中的任務設定為已選擇任務。
執行任務。
將事件迴圈中當前執行任務設定為null。
將已經執行完成的任務從任務佇列中刪除。
microtasks步驟:進入microtask檢查點。
更新介面渲染。
返回第一步。
執行進入microtask檢查點時,使用者代理會執行以下步驟:
設定microtask檢查點標誌為true。
當事件迴圈microtask執行不為空時:選擇一個最先進入的microtask佇列的microtask,將事件迴圈的microtask設定為已選擇的microtask,執行microtask,將已經執行完成的microtask為null,移出microtask中的microtask。
清理IndexDB事務
設定進入microtask檢查點的標誌為false。
執行棧在執行完 同步任務 後,檢視 執行棧 是否為空,如果執行棧為空,就會去檢查 微任務 (microTask)佇列是否為空,如果為空的話,就執行Task(巨集任務),否則就一次性執行完所有微任務。
每次單個 巨集任務 執行完畢後,檢查 微任務 (microTask)佇列是否為空,如果不為空的話,會按照 先入先 出的規則全部執行完 微任務 (microTask)後,設定 微任務 (microTask)佇列為null,然後再執行 巨集任務 ,如此迴圈。
舉個例子
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
首先我們劃分幾個分類:
第一次執行:
Tasks:run script、 setTimeout callback
Microtasks:Promise then
JS stack: script
Log: script start、script end。
執行同步程式碼,將巨集任務(Tasks)和微任務(Microtasks)劃分到各自佇列中。
第二次執行:
Tasks:run script、 setTimeout callback
Microtasks:Promise2 then
JS stack: Promise2 callback
Log: script start、script end、promise1、promise2
執行同步程式碼後,檢測到微任務(Microtasks)佇列中不為空,執行Promise1,執行完成Promise1後,呼叫Promise2.then,放入微任務(Microtasks)佇列中,再執行Promise2.then。
第三次執行:
Tasks:setTimeout callback
Microtasks:
JS stack:
Log: script start、script end、promise1、promise2、setTimeout
當微任務(Microtasks)佇列中為空時,執行巨集任務(Tasks),執行setTimeout callback,列印日誌。
第四次執行:
Tasks:setTimeout callback
Microtasks:
JS stack:
Log: script start、script end、promise1、promise2、setTimeout
清空 Tasks 佇列和JS stack。
以上執行幀動畫可以檢視 Tasks, microtasks, queues and schedules
再舉個例子
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
這裡需要先理解async/await。
async/await在底層轉換成了promise和then回撥函式。
也就是說,這是promise的語法糖。
每次我們使用await, 直譯器都建立一個promise物件,然後把剩下的async函式中的操作放到then回撥函式中。
async/await的實現,離不開Promise。從字面意思來理解,async是“非同步”的簡寫,而await是async wait的簡寫可以認為是等待非同步方法執行完成。