面試和筆試題目中,常常會出現'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
咱們多多少少都應該據說過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
二、 遇到setTimeout log: script start
三、 遇到Promise
四、 執行最後一行 log: script start | script end
四、 同步任務執行完畢,彈出相應的stack log: script start | script end
五、 同步任務最後是microTasks,JS stack壓入callback log: script start | script end | promise1
log: script start | script end | promise1 | promise2
八、 第一個Tasks結束,彈出 log: script start | script end | promise1 | promise2
九、 下一個Tasks log: script start | script end | promise1 | promise2 | setTimeout
好了,結束了,這就比以前的理解"promise比setTimeout快,異步先執行promise,再執行setTimeout"就深入的多。 由於promise所創建的回掉函數是壓入了mircroTasks
隊列中,它仍然屬於當前的Task,而setTimeout
則是至關於在Task序列中添加了新的任務
好了,有了剛纔的認識和鋪墊,接下來經過一個更加複雜的例子來熟悉JS事件處理的一個過程。
如今有這樣一個頁面結構:
<div class="outer">
<div class="inner"></div>
</div>
複製代碼
// 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之中。 所以
一、點擊事件觸發
二、觸發點擊事件的函數,函數執行,壓入JS stack
三、遇到setTimeout,壓入Tasks隊列
四、遇到promise,壓入Microtasks
五、遇到 outer.setAttribute,觸發mutation
六、onclick函數執行完畢,出JS stack
七、這個時候,JS stack爲空,執行Microtasks
八、microtasks順序執行
接下來是重點,當microtasks爲空,該執行下一個Tasks(setTimeout)了嗎?並無,由於js事件流中的冒泡被觸發,也就是在外面的一層Div也會觸發click函數,所以咱們把剛纔的步驟再走一遍。
過程省略,結果爲 九、冒泡走一遍的結果爲
click
promise
mutate
click
promise
mutate
十、 第一個Tasks完成,出棧
click
promise
mutate
click
promise
mutate
timeout
十一、 第二個Tasks完成,出棧
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)一樣的代碼,得出的結果是不一樣的哦。關鍵在於,對與 job
和microTasks
之間的一個聯繫是很模糊的。 可是咱們就按照Chrome的實現來理解吧。
仍是剛纔那道題,只不過,我不用鼠標點擊了,而是直接執行函數
inner.click()
複製代碼
若是這樣,結果會同樣嗎?
答案是:
click
click
promise
mutate
promise
timeout
timeout
複製代碼
What!!??我怎麼感受我白學了? 不着急,看下此次的過程是這樣的,首先最大的不一樣在於,咱們在函數最底部加了一個執行inner.click()
,這樣子,這個函數執行的過程,都是同步序列裏的,因此此次的task的起點就在Run scripts:
一、不一樣與鼠標點擊,咱們執行函數後,進入函數內部執行
click
二、遇到setTimeout和promise&mutation
click
三、接下來關鍵,冒泡的時候,由於咱們並無執行完當前的script,還在inner.click()
這個函數執行之中,所以當onclick
結束,開始冒泡時,script並無結束
click
click
四、冒泡階段重複以前內容
click
click
注意第二次沒有增長mutation,由於已經有一個在渲染的了
五、inner.click()執行完畢,執行Microtasks
click
click
promise
六、按理論執行
click
click
promise
mutate
....後面的就不解釋了,Microtasks依次出棧,接着Tasks順序執行。
Jake老師的文章,對這個的解析和深刻實在使人佩服,我也在面試中因把event loop解釋的較爲詳盡而被面試官確定,因此若是對異步以及event loop有疑惑的,能夠好好的消化下這個內容,一塊兒進步!