JS的Event Loop 和 microTask

面試和筆試題目中,常常會出現'promise','setTimeout'等函數混合出現時候的運行順序問題。 咱們都知道這些異步的方法會在當前任務執行結束以後調用,但爲何'promise'會在'setTimeout'以前執行? 具體的實現原理是什麼?javascript

有和我同樣正在爲秋招offer奮鬥的小夥伴,歡迎到github獲取更多個人總結和踩過的坑,一塊兒進步→→→→傳送門html

問題的提出

上面問題的答案,都在文章《Tasks, microtasks, queues and schedules》講的很是透徹。 建議英文能夠的同窗直接看這篇文章,就不要看我這個「筆記」了。( 之因此叫筆記,由於大部份內容出自文章,可是又不是按字翻譯 )前端

如下的題目是咱們刷題能夠常常看到的一個常規題目:html5

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');
複製代碼

幾乎每一個前端er均可以堅決果斷的給出答案:java

script start
script end
promise1
promise2
setTimeout
複製代碼

問題來了,爲何promise的異步執行會在setTimeout以前,甚至setTimeout設置的延時是0都不行。 還有在Vue中,咱們經常使用的nextTick()函數原理中,說的microtasks是什麼東西? 一切的解釋都在開頭給的文章中。git

ps: 再次再次聲明,這篇文章仍然是我記得筆記,原文比我寫的好得多,英文能夠的小夥伴強烈推薦看原文。github

js異步實現原理

咱們多多少少都應該據說過event loop,js是單線程的,經過異步它變得很是強大,而實現異步主要就是經過將異步的內容壓入tasks,當前任務執行結束以後,再執行tasks中的callback。面試

Tasks,是一個任務隊列,Js在執行同步任務的時候,只要遇到了異步執行和函數,都會把這個內容壓入Tasks中,而後在當前同步任務完成後,再去Tasks中執行相應的回調。 舉個例子,好比剛纔代碼中的setTimeout,當遇到這個函數,總會跟一個異步執行的任務(callback),那麼這個時候,Tasks隊列裏,除了當前正在執行的script以外,會在後面壓入一個setTimeout callback, 而這個callback的調用時機,就是在當前同步任務完成以後,纔會調用。這就是爲何,'setTimeout' 會出如今'script end'以後了。chrome

MicroTasks,說一些這個,這個和setTimeout不一樣,由於它是在當前Task完成後,就當即執行的,或者能夠理解成,'microTasks老是在當前任務的最後執行'。 另外,還有一個很是重要的特性是: 若是當前JS stack若是爲空的時候(好比咱們綁定了click事件後,等待和監聽click時間的時候,JS stack就是空的),一會當即執行。 關於這一點,以後有個例子會具體說明,先往下看。promise

那麼MicroTasks隊列主要是promise和mutation observer 的回掉函數生成

用新的理論來解釋下

好了,剛纔大概說了幾個概念,那麼一開始的例子,到底發生了什麼?

talk is cheap, show me a animation!!---我本身說的

下面的動畫說明對整個過程進行了說明:

原文中的動態演示

一、 程序執行 log: script start

  • Tasks: Run script
  • JS stack: script

二、 遇到setTimeout log: script start

  • Tasks: Run script | setTimeout callback
  • JS stack: script

三、 遇到Promise

  • Tasks: Run script | setTimeout callback
  • Microtasks: promise then
  • JS stack: script

四、 執行最後一行 log: script start | script end

  • Tasks: Run script | setTimeout callback
  • Microtasks: promise then
  • JS stack: script

四、 同步任務執行完畢,彈出相應的stack log: script start | script end

  • Tasks: Run script | setTimeout callback
  • Microtasks: promise then
  • JS stack:

五、 同步任務最後是microTasks,JS stack壓入callback log: script start | script end | promise1

  • Tasks: Run script | setTimeout callback
  • Microtasks: promise then | promise then
  • JS stack: promise1 calback 六、 promise返回新的promise,壓入microTasks,繼續執行 log: script start | script end | promise1 | promise2
  • Tasks: Run script | setTimeout callback
  • Microtasks: promise then
  • JS stack: promise2 calback

八、 第一個Tasks結束,彈出 log: script start | script end | promise1 | promise2

  • Tasks: setTimeout callback
  • Microtasks:
  • JS stack:

九、 下一個Tasks log: script start | script end | promise1 | promise2 | setTimeout

  • Tasks: setTimeout callback
  • Microtasks:
  • JS stack: setTimeout callback

好了,結束了,這就比以前的理解"promise比setTimeout快,異步先執行promise,再執行setTimeout"就深入的多。 由於promise所創建的回掉函數是壓入了mircroTasks隊列中,它仍然屬於當前的Task,而setTimeout則是至關於在Task序列中添加了新的任務

一個更復雜的例子

好了,有了剛纔的認識和鋪墊,接下來經過一個更加複雜的例子來熟悉JS事件處理的一個過程。

如今有這樣一個頁面結構:

<div class="outer">
  <div class="inner"></div>
</div>

複製代碼

js代碼以下,如今若是點擊裏面的方塊,控制檯會輸出什麼呢? 在線實例

// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

// Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
  console.log('mutate');
}).observe(outer, {
  attributes: true
});

// Here's a click listener…
function onClick() {
  console.log('click');

  setTimeout(function() {
    console.log('timeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('promise');
  });

  outer.setAttribute('data-random', Math.random());
}

// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
複製代碼

這裏先把正確答案公佈,按照以前的理論,正確答案應該是:

click
promise
mutate
click
promise
mutate
timeout
timeout
複製代碼

固然,不一樣瀏覽器,對於event loop的實現會稍有不一樣,這個是chrome下打印出來的結果,具體的其餘形式仍是推薦你們看原文了。

下面分析下,爲何是上面的順序呢?

代碼分析

按照剛纔的結論:

click事件顯然是一個Task,Mutation observer和Promise是在microTasks隊列中的,而setTimeout會被安排在Tasks之中。 所以

一、點擊事件觸發

  • Tasks: Dispatch click
  • Microtasks:
  • JS stack:

二、觸發點擊事件的函數,函數執行,壓入JS stack

  • Tasks: Dispatch click
  • Microtasks:
  • JS stack: onClick
  • Log: 'click'

三、遇到setTimeout,壓入Tasks隊列

  • Tasks: Dispatch click | setTimeout callBack
  • Microtasks:
  • JS stack: onClick
  • Log: 'click'

四、遇到promise,壓入Microtasks

  • Tasks: Dispatch click | setTimeout callBack
  • Microtasks: Promise.then
  • JS stack: onClick
  • Log: 'click'

五、遇到 outer.setAttribute,觸發mutation

  • Tasks: Dispatch click | setTimeout callBack
  • Microtasks: Promise.then | Mutation observers
  • JS stack: onClick
  • Log: 'click'

六、onclick函數執行完畢,出JS stack

  • Tasks: Dispatch click | setTimeout callBack
  • Microtasks: Promise.then | Mutation observers
  • JS stack:
  • Log: 'click'

七、這個時候,JS stack爲空,執行Microtasks

  • Tasks: Dispatch click | setTimeout callBack
  • Microtasks: Promise.then | Mutation observers
  • JS stack: PromiseCallback
  • Log: 'click' 'promise'

八、microtasks順序執行

  • Tasks: Dispatch click | setTimeout callBack
  • Microtasks: Mutation observers
  • JS stack: Mutation callback
  • Log: 'click' 'promise' 'mutate'

接下來是重點,當microtasks爲空,該執行下一個Tasks(setTimeout)了嗎?並無,由於js事件流中的冒泡被觸發,也就是在外面的一層Div也會觸發click函數,所以咱們把剛纔的步驟再走一遍。

過程省略,結果爲 九、冒泡走一遍的結果爲

  • Tasks: Dispatch click | setTimeout callBack | setTmeout callback(outer)
  • Microtasks: Mutation observers
  • JS stack: Mutation callback
  • Log: click promise mutate click promise mutate

十、 第一個Tasks完成,出棧

  • Tasks: setTimeout callBack | setTmeout callback(outer)
  • Microtasks:
  • JS stack: setTimeout callback
  • Log: click promise mutate click promise mutate timeout

十一、 第二個Tasks完成,出棧

  • Tasks: setTmeout callback(outer)
  • Microtasks:
  • JS stack: setTimeout(outer) callback
  • Log: click promise mutate click promise mutate timeout timeout

結束了

因此這裏的重點是什麼? 是MicroTasks的執行時機: 見縫插針,它不必定就必須在Tasks的最後,只要JS stack爲空,就能夠執行 這條規則出處在

If the stack of script settings objects is now empty, perform a microtask checkpoint
— HTML: Cleaning up after a callback step 3

另外一方面,ECMA也對此有過說明

Execution of a Job can be initiated only when there is no running execution context and the execution context stack is empty…
— ECMAScript: Jobs and Job Queues

可是對於其餘瀏覽器(firefox safari ie)一樣的代碼,得出的結果是不一樣的哦。關鍵在於,對與 jobmicroTasks之間的一個聯繫是很模糊的。 可是咱們就按照Chrome的實現來理解吧。

最後一關

仍是剛纔那道題,只不過,我不用鼠標點擊了,而是直接執行函數

inner.click()
複製代碼

若是這樣,結果會同樣嗎?

答案是:

click
click
promise
mutate
promise
timeout 
timeout
複製代碼

What!!??我怎麼感受我白學了? 不着急,看下此次的過程是這樣的,首先最大的不一樣在於,咱們在函數最底部加了一個執行inner.click(),這樣子,這個函數執行的過程,都是同步序列裏的,因此此次的task的起點就在Run scripts:

一、不一樣與鼠標點擊,咱們執行函數後,進入函數內部執行

  • Tasks: Run scripts
  • Microtasks:
  • JS stack: script | onClick
  • Log: click

二、遇到setTimeout和promise&mutation

  • Tasks: Run scripts | setTimeout callback
  • Microtasks: Promise.then | Mutation Observers
  • JS stack: script | onClick
  • Log: click

三、接下來關鍵,冒泡的時候,由於咱們並無執行完當前的script,還在inner.click()這個函數執行之中,所以當onclick結束,開始冒泡時,script並無結束

  • Tasks: Run scripts | setTimeout callback
  • Microtasks: Promise.then | Mutation Observers
  • JS stack: script | onClick(這是冒泡的click,第一次click已經結束)
  • Log: click click

四、冒泡階段重複以前內容

  • Tasks: Run scripts | setTimeout callback |setTimeout callback(outer)
  • Microtasks: Promise.then | Mutation Observers |promise.then
  • JS stack: script | onClick(這是冒泡的click,第一次click已經結束)
  • Log: click click

注意第二次沒有增長mutation,由於已經有一個在渲染的了

五、inner.click()執行完畢,執行Microtasks

  • Tasks: Run scripts | setTimeout callback |setTimeout callback(outer)
  • Microtasks: Promise.then | Mutation Observers |promise.then
  • JS stack:
  • Log: click click promise

六、按理論執行

  • Tasks: Run scripts | setTimeout callback |setTimeout callback(outer)
  • Microtasks: Mutation Observers |promise.then
  • JS stack:
  • Log: click click promise mutate....

後面的就不解釋了,Microtasks依次出棧,接着Tasks順序執行。

總結

Jake老師的文章,對這個的解析和深刻實在使人佩服,我也在面試中因把event loop解釋的較爲詳盡而被面試官確定,因此若是對異步以及event loop有疑惑的,能夠好好的消化下這個內容,一塊兒進步!

相關文章
相關標籤/搜索