JS 做爲瀏覽器腳本語言,爲了不復雜的同步問題(例如用戶操做事件以及操做DOM),這就決定了被設計成單線程語言,並且也將會一直保持是單線程的。而在單線程中如果遇到了耗時的操做(IO,定時器,網絡請求)將會一直等待,CPU利用率將會大打折扣,時間大量浪費。因此須要設計一種方案讓一些耗時的操做放在一邊等待,讓後面的函數先執行,因而有了EventLoop的設計。html
將任務分爲兩種:html5
阮一峯老師《JavaScript 運行機制詳解:再談Event Loop》程序員
(1)全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。vim
(2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。segmentfault
(3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。瀏覽器
(4)主線程不斷重複上面的第三步。網絡
任務都會按順序進入調用棧(call stack),即圖1-1的stack,而後按棧的順序依次執行。若全是同步任務,就會正常地順序執行。當遇到異步任務時(其實就是執行到了一個耗時的任務,它發起後,須要它的回調函數等待拿到結果以後才繼續進行)將會放到WebAPIs中(圖1-1),等待這個耗時操做返回結果,也有網友把這個 WebAPIs 稱之爲 Event Table。若是異步任務在WebAPIs中等待有告終果(好比setTimeout的時間截止了,xhr獲得響應結果了,用戶click事件發生了),就會將這個結果做爲一個事件置於任務隊列中。 【或者稱之爲:註冊回調函數】異步
那麼任務隊列又是什麼?我的認爲就是圖中的callback queue,或稱之爲 Event Queue 。就是存放了各類耗時操做最後響應結果的各個事件(說白了,就是已經拿到結果的,就會從WebAPIs放到任務隊列裏來)函數
圖 1-1 轉自Philip Roberts的演講《Help, I'm stuck in an event-loop》工具
搞懂上面兩段話後,就能夠談EventLoop的做用了:
以上過程是不斷循環的,js引擎中,存在一個叫monitoring process的進程,這個進程會不斷的檢查主線程的執行狀況,一旦爲空,就會去任務隊列檢查有哪些待執行的函數。這裏的整個過程能夠參考 一個工具 loupe 對整個調用過程進行查看。
圖 1-2 loupe, 也是從其餘地方發現的這個東西,很直觀
針對call stack調用棧多說一句:通俗地講,將調用棧比喻爲程序員,各個任務比喻爲需求,任務隊列比喻爲總監。當總監提需求時,程序員就要交接需求過來,而後完成它。若是沒有需求,就一直等待總監給需求。給了就作,不給就等。
搞懂同步任務與異步任務的具體執行流程後,再談談爲何要設計宏任務和微任務。
頁面渲染事件,各類IO的完成事件等隨時被添加到任務隊列中,一直會保持先進先出的原則執行,咱們不能準確地控制這些事件被添加到任務隊列中的位置。可是這個時候忽然有高優先級的任務須要儘快執行,那麼若只有一種類型的任務就不合適了,因此引入了微任務隊列。
至此,任務隊列已被分爲:
圖 2-1 微任務Microtask Queue的加入
首先列舉一下哪些是宏任務、哪些是微任務
宏任務:
微任務:
緊接着第一節裏說的EventLoop,當時沒有考慮什麼宏任務微任務,如今再加入微任務的概念再來考慮整個流程:
依舊是:兩個任務隊列(宏、微)只有有任務,那麼主進程的調用棧就會調過去執行,沒有任務的話,主進程就一直等着,直到又有任務。
圖 2-2 宏任務與微任務的執行順序
注意的是,圖2-2看起來是宏任務先執行,微任務後執行,這僅僅是宏任務與微任務的前後次序,但不表明宏任務優先級比微任務高。事實是微任務的優先級是高於宏任務的。由於微任務實際上是產生於宏任務的,不可能憑空產生微任務,也就不可能一開始就出現幾個微任務。在本次宏任務產生微任務後,將會在下次宏任務執行以前,優先執行這些微任務。天然也就映證了設計微任務的初衷:爲了讓某些任務儘快執行。
總結完整的EventLoop流程:
微任務在本次宏任務以後執行,在本次渲染以前執行,在下次宏任務以前執行。(宏任務 -> 微任務 -> 渲染 -> 宏任務)
// 知乎做者:Miku // 連接:https://zhuanlan.zhihu.com/p/257069622
// 注意:代碼中的process.netxTick 函數存在於Node.js中
console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5'); }); }); process.nextTick(function() { console.log('6'); }); new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { console.log('8'); }); setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }); new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12'); }); });
第一輪循環:
1)、首先打印 1 2)、接下來是setTimeout是異步任務且是宏任務,加入宏任務暫且記爲 setTimeout1 3)、接下來是 process 微任務 加入微任務隊列 記爲 process1 4)、接下來是 new Promise 裏面直接 resolve(7) 因此打印 7 後面的then是微任務 記爲 then1 5)、setTimeout 宏任務 記爲 setTimeout2 第一輪循環打印出的是 1 7 當前宏任務隊列:setTimeout1, setTimeout2 當前微任務隊列:process1, then1,第二輪循環:
1)、執行全部微任務 2)、執行process1,打印出 6 3)、執行then1 打印出8 4)、微任務都執行結束了,開始執行第一個宏任務 5)、執行 setTimeout1 也就是 第 3 - 14 行 6)、首先打印出 2 7)、遇到 process 微任務 記爲 process2 8)、new Promise中resolve 打印出 4 9)、then 微任務 記爲 then2 第二輪循環結束,當前打印出來的是 1 7 6 8 2 4 當前宏任務隊列:setTimeout2 當前微任務隊列:process2, then2第三輪循環:
1)、執行全部的微任務 2)、執行 process2 打印出 3 3)、執行 then2 打印出 5 4)、執行第一個宏任務,也就是執行 setTimeout2 對應代碼中的 25 - 36 行 5)、首先打印出 9 6)、process 微任務 記爲 process3 7)、new Promise執行resolve 打印出 11 8)、then 微任務 記爲 then3 第三輪循環結束,當前打印順序爲:1 7 6 8 2 4 3 5 9 11 當前宏任務隊列爲空 當前微任務隊列:process3,then3第四輪循環:
1)、執行全部的微任務 2)、執行process3 打印出 10 3)、執行then3 打印出 12 代碼執行結束: 最終打印順序爲:1 7 6 8 2 4 3 5 9 11 10 12