咱們一般說 JavaScript 是單線程的,其實是指在 JS 引擎中負責解釋和執行 JS 代碼的線程只有一個,通常成爲主線程,在這種前提下,爲了讓用戶的操做不存在阻塞感,前端 APP 的運行須要依賴於大量的異步過程,因此固然瀏覽器中還存在一些其餘的線程,好比處理 http 請求的線程、處理 DOM 事件的線程、定時器線程、處理文件讀寫的 I/O 線程等等。前端
一個異步過程一般是這樣的:web
異步過程當中,工做線程在異步操做完成後須要通知主線程,那麼這個通知機制是怎樣實現的呢?答案是利用消息隊列和事件循環。消息隊列是一個先進先出的隊列,裏面存放着各類消息,咱們能夠簡單的理解爲消息就是註冊異步任務時添加的回調函數;事件循環是指主線程重複從消息隊列中獲取消息、執行回調的過程,之因此稱爲事件循環,就是由於它常常被用相似以下方式來實現:promise
while (queue.waitForMessage()) { queue.processNextMessage(); }
消息隊列是一個存儲着待執行任務的隊列,其中的任務嚴格按照時間前後順序執行,排在隊頭的任務將會率先執行,而排在隊尾的任務會最後執行。消息隊列每次僅執行一個任務,在該任務執行完畢以後,再執行下一個任務。執行棧則是一個相似於函數調用棧的運行容器,當執行棧爲空時 JS 引擎便檢查消息隊列,若是不爲空消息隊列便將第一個任務壓入執行棧中運行。瀏覽器
下面咱們來看下述代碼來驗證咱們的想法:異步
setTimeout(function() { console.log(4) }, 0); new Promise(function(resolve) { console.log(1) for (var i = 0; i < 10000; i++) { i == 9999 && resolve() } console.log(2) }).then(function() { console.log(5) }); console.log(3);
按道理來講,執行 setTimeout 時由於延遲爲0,因此 console.log(4) 直接插入至消息隊列;建立 Promise 實例時同步執行其函數體內的代碼,先打印 1,再循環10000次後執行 resolve 將 then 中的回調函數 console.log(5) 插入至消息隊列,而後打印 2;最後執行 console.log(3) 打印 3;在主線程執行完成後讀取消息隊列,依次打印4和5。函數
上面的想法固然是比較天真的,實際上瀏覽器中僅有一個事件循環,而後消息隊列是能夠有多個的。
macro-queue: script (總體代碼), setTimeout, setInterval, setImmediate, I/O, UI Rendering
micro-queue: process.nextTick, Promise, Object.observe, MutationObserveroop
而且 micro-queue 的任務優先級高於 macro-queue 的任務優先級,這兩個任務隊列執行順序以下:取1個 macro-task 執行之,而後把全部 micro-task 順序執行完,再取 macro-task 中的下一個任務,以此類推依次進行。線程
優先級:process.nextTick > promise.then > setTimeout > setImmediate
Tip:process.nextTick 永遠大於 promise.then 緣由其實很簡單:在 NodeJS 中,_tickCallback 在每一次執行完 TaskQueue 中的一個任務後被調用,而在這個_tickCallback 中實質上幹了兩件事:code
總結:瀏覽器環境通常只能有一個事件循環(實際上有兩類:browsing contexts 和 web workers),而一個事件循環能夠多個任務隊列,每一個任務都有一個任務源。相同任務源的任務,只能放到一個任務隊列中;不一樣任務源的任務,能夠放到不一樣任務隊列中。舉個栗子,客戶端可能實現一個包含鼠標鍵盤事件的任務隊列,還有其餘的任務隊列,而給鼠標鍵盤事件的任務隊列更高優先級,例如75%的可能性執行它,這樣就能保證流暢的交互性,並且別的任務也能執行到。server
至此,再返回去看以前的代碼就不難分析出:代碼執行開始把 setTimeout 的回調插入至 macro-queue 中,而打印完1後把 promise.then 的回調函數插入至 micro-queue 中,總體代碼執行完後,按照消息隊列的優先級,先執行 micro-task 即打印5,最後執行 macro-task 即打印4。