JS中EventLoop、宏任務與微任務的我的理解

 爲何要EventLoop?

  JS 做爲瀏覽器腳本語言,爲了不復雜的同步問題(例如用戶操做事件以及操做DOM),這就決定了被設計成單線程語言,並且也將會一直保持是單線程的。而在單線程中如果遇到了耗時的操做(IO,定時器,網絡請求)將會一直等待,CPU利用率將會大打折扣,時間大量浪費。因此須要設計一種方案讓一些耗時的操做放在一邊等待,讓後面的函數先執行,因而有了EventLoop的設計。html

  將任務分爲兩種:html5

  • 同步任務
  • 異步任務
  1. 定時器都是異步操做
  2. 事件綁定都是異步操做
  3. AJAX中通常採起的異步操做(雖然也能夠同步)
  4. 回調函數(不嚴謹的異步)

 阮一峯老師《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的完成事件等隨時被添加到任務隊列中,一直會保持先進先出的原則執行,咱們不能準確地控制這些事件被添加到任務隊列中的位置。可是這個時候忽然有高優先級的任務須要儘快執行,那麼若只有一種類型的任務就不合適了,因此引入了微任務隊列。

  至此,任務隊列已被分爲:

  • 宏任務隊列,即上文說的任務隊列,callback queue,用於存放宏任務
  • 微任務隊列,再開闢一個隊列,用於存放微任務

圖 2-1 微任務Microtask Queue的加入

  首先列舉一下哪些是宏任務、哪些是微任務

  宏任務

  • script(主代碼)
  • setTimeout()
  • setInterval()
  • postMessage
  • I/O
  • UI交互事件
  • setImmediate(Node.js)
  • requestAnimationFrame(瀏覽器)

  微任務

  • new Promise().then(回調)
  • MutationObserver(html5 新特性)
  • process.nextTick(Node.js)在當前"執行棧"的尾部----下一次Event Loop(主線程讀取"任務隊列")以前----觸發回調函數。也就是說,它指定的任務老是發生在全部異步任務以前

  緊接着第一節裏說的EventLoop,當時沒有考慮什麼宏任務微任務,如今再加入微任務的概念再來考慮整個流程:

  1. 依舊是在調用棧和任務隊列中輪詢。(此時的任務隊列指的是宏任務隊列)
  2. 調用棧爲空後,優先檢查微任務隊列,若是微任務隊列中存在事件,則加入到調用棧中進行執行(爲何先詢問的是微任務隊列而不是宏任務隊列,在後面解釋)
    • 注:若是在執行微任務隊列中的函數時,產生了新的微任務(好比then函數嵌套),則會繼續在本次執行中執行(就是說若是期間一直有微任務產生,那就會永遠卡在微任務隊列執行)
  3. 若是微任務隊列爲空,那就取宏任務隊列中的事件加入到調用棧中進行執行
  4. 若在執行宏任務的時候,產生了新的微任務,就會將該微任務加入到微任務隊列,該微任務隊列將會在下一次宏任務執行以前執行,如圖2-2。
  5. 循環。

  依舊是:兩個任務隊列(宏、微)只有有任務,那麼主進程的調用棧就會調過去執行,沒有任務的話,主進程就一直等着,直到又有任務。

圖 2-2 宏任務與微任務的執行順序

  注意的是,圖2-2看起來是宏任務先執行,微任務後執行,這僅僅是宏任務與微任務的前後次序,但不表明宏任務優先級比微任務高。事實是微任務的優先級是高於宏任務的。由於微任務實際上是產生於宏任務的,不可能憑空產生微任務,也就不可能一開始就出現幾個微任務。在本次宏任務產生微任務後,將會在下次宏任務執行以前,優先執行這些微任務。天然也就映證了設計微任務的初衷:爲了讓某些任務儘快執行。

  總結完整的EventLoop流程:

  1. 執行一個宏任務(調用棧中沒有就從宏、微任務隊列中獲取)
  2. 執行過程當中若是遇到微任務,就將它添加到微任務的任務隊列中
  3. 宏任務執行完畢後,當即執行當前微任務隊列中的全部微任務(依次執行)
  4. 當前微任務執行完畢,開始檢查渲染,而後GUI線程接管渲染
  5. 渲染完畢後,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)

  微任務在本次宏任務以後執行,在本次渲染以前執行,在下次宏任務以前執行。(宏任務 -> 微任務 -> 渲染 -> 宏任務)

 包含宏任務、微任務的異步代碼分析:

// 知乎做者: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

 參考

相關文章
相關標籤/搜索