在寫代碼的時候常常思考一個問題,究竟是那個函數先執行,自己JavaScript是一門單線程的語言,意思就是按照順序執行。可是加入一些setTimeout和promise的函數來又實現了異步操做,經常我會寫一個setTimeout(fn,0),他會當即執行嗎?promise
首先咱們先來看一段代碼:異步
<script>
console.log("Start");
setTimeout(function(){
console.log("SetTimeout");
},0);
new Promise(function(resolve,reject){
console.log("Promise");
resolve();
}).then(function(){
console.log("Then");
});
console.log("End");
<script> 複製代碼
這些日誌的打印順序是:async
Start
Promise
End
Then
SetTimeout
複製代碼
這是爲何函數
首先,咱們知道JavaScript的一大特色就是單線程,而這個線程中擁有惟一的一個事件循環。oop
一個線程中,事件循環是惟一的,可是任務隊列能夠擁有多個。ui
任務隊列又分爲macro-task(宏任務)與micro-task(微任務),在最新標準中,它們被分別稱爲task與jobs。spa
宏任務線程
微任務日誌
事件循環的順序,決定js代碼的執行順序。一段代碼塊就是一個宏任務。進入總體代碼(宏任務)後,開始第一次循環。接着執行全部的微任務。而後再次從宏任務開始,找到其中一個任務隊列執行完畢,再執行全部的微任務。code
主線程(宏任務) => 微任務 => 宏任務 => 主線程
下圖是簡易版的事件循環:
因此在上面的代碼中宏任務有script代碼塊,setTimeout,微任務有Promise
事件循環流程分析以下:
script
做爲第一個宏任務進入主線程,遇到console.log,輸出Start
。setTimeout
,其回調函數被分發到宏任務Event Queue中。Promise
,new Promise直接執行,輸出Promise。then被分發到微任務Event Queue中。End
。script
做爲第一個宏任務執行結束,看看有哪些微任務?咱們發現了then
在微任務Event Queue裏面,執行setTimeout
對應的回調函數,當即執行。提升下難度在來一段較爲複雜的代碼來檢驗是否已經基本瞭解了事件循環的機制
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout1');
}, 200);
setTimeout(function() {
console.log('setTimeout2');
new Promise(function(resolve) {
resolve();
}).then(function() {
console.log('then1')
})
new Promise(function(resolve) {
console.log('Promise1');
resolve();
}).then(function() {
console.log('then2')
})
},0)
async1();
new Promise(function(resolve) {
console.log('promise2');
resolve();
}).then(function() {
console.log('then3');
});
console.log('script end');
複製代碼
第一輪事件循環流程分析以下:
總體script做爲第一個宏任務進入主線程,async1(),和async12()函數申明,但並無執行,遇到console.log
輸出script start
。
繼續向下執行,遇到setTimeout
,把它的回調函數放入宏任務Event Queue。(ps:暫且叫他setTimeout1)
宏任務 | 微任務 |
---|---|
setTimeout1 | 1 |
繼續向下執行,又遇到一個setTimeout
,繼續將他放入宏任務Event Queue。(ps:暫且叫他setTimeout2)
宏任務 | 微任務 |
---|---|
setTimeout1 | |
setTimeout2 |
遇到執行async1(), 進入async
的執行上下文以後,遇到console.log
輸出 async1 start
而後遇到await async2(),因爲()
的優先級高,全部當即執行async2()
,進入async2()
的執行上下文。
看到console.log
輸出async2
,以後沒有返回值,結束函數,返回undefined,返回async1
的執行上下文的await undefined,因爲async
函數使用await
後得語句會被放入一個回調函數中,因此把下面的放入微任務Event Queue中。
宏任務 | 微任務 |
---|---|
setTimeout1 | async1 => awati 後面的語句 |
setTimeout2 |
結束async1()
遇到Promise
,new Promise直接執行,輸出Promise2
。then
後面的函數被分發到微任務Event Queue中
宏任務 | 微任務 |
---|---|
setTimeout1 | async1 => awati 後面的語句 |
setTimeout2 | new Promise() => 後的then |
執行完Promise()
,遇到console.log
,輸出script end
,這裏一個宏任務代碼塊執行完畢。
在主線程執行的過程當中,事件觸發線程一直在監聽着異步事件, 當主線程空閒下來後,若微任務隊列中有任務未執行,執行的事件隊列(Event Queue)中有微任務,遇到new Promise()
後面的回調函數,執行代碼,輸出then3
。
看到 async1
中await
後面的回調函數,執行代碼,輸出async1 end
(注意:若是倆個微任務的優先級相同那麼任務隊列自上而下執行,可是promise的優先級高於async,因此先執行promise後面的回調函數)
自此,第一輪事件循環正式結束,這一輪的結果是輸出:script start => async1 start => async2 => promise2 => script end => then3 => async1 end
宏任務 | 微任務 |
---|---|
setTimeout1 | |
setTimeout2 |
那麼第二輪時間循環從setTimeout宏任務開始:
setTimeout和setInterval的運行機制是,將指定的代碼移出本次執行,等到下一輪Event Loop時,再檢查是否到了指定時間。若是到了,就執行對應的代碼;若是不到,就等到再下一輪Event Loop時從新判斷。由於setTimeout1有200ms的延時,並沒到達指定時間,因此先執行setTimeout2這個宏任務
進入到setTimeout2,遇到console.log
首先輸出setTimeout2
;
遇到Promise
,new Promise直接執行。then
後面的函數被分發到微任務Event Queue中
宏任務 | 微任務 |
---|---|
setTimeout1 | new Promise() => 後的then1 |
再次遇到Promise
,new Promise直接執行輸出promise1
。then
後面的函數被分發到微任務Event Queue中
宏任務 | 微任務 |
---|---|
setTimeout1 | new Promise() => 後的then1 |
空 | new Promise() => 後的then2 |
主線程執行執行空閒,開始執行微任務隊列中依次輸出then1
和then2
。
第二輪事件循環正式結束。第二輪依次輸出promise1 => then1 => then2
如今任務隊列中只有個延時200ms的setTimeout1,在到達200ms後執行setTimeout的回調函數輸出setTimeout1
時間循環結束
整段代碼,完整的輸出爲script start => async1 start => async2 => promise2 => script end => then3 => async1 end => promise1 => then1 => then2 => setTimeout1
因此在咱們寫下setTimeout(fn,0)
的時候他並非在當時當即執行,是從下一個Event loop
開始執行,便是等當前全部腳本執行完再運行,就是"儘量早"。