題意描述: 給出以下代碼的運行結果。
輸入:
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('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
輸出:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
解題思路:
Alice : 這道題你做錯了呀,來說說你是怎麼想的吧。
Bob: 我知道 JavaScript 中的事件有 微任務 和 宏任務 之分, 每個宏任務內部,總是微任務先執行,然後才執行這個宏任務。
Alice: 你的意思是 每個 宏任務 “綁定” 了一些微任務,在這些微任務執行完之後纔會執行這個宏任務 ?
Bob: 對,但是我不知道 微任務與微任務之間,宏任務與宏任務之間的 執行順序該怎麼確定。
Alice; 這個就說來話長了,我慢慢講給你聽,可以先告訴你,你理解的宏任務和微任務的執行順序是不對的,一個宏任務可能有若干個微任務,在宏任務執行結束之後纔會去執行這些微任務。這道題考察的知識點還是挺多的,只知道微任務和宏任務肯定是答不出來的,你看裏面 又有 async await
又有 setTimeout
又有 Promise
,只有對 JavaScript 的 事件循環 掌握了才能答得出來。
Bob: 那你快說呀,我洗耳恭聽。
Alice: 那就先從 任務隊列說起吧。1)JavaScript 中的任務分爲 同步任務 和 異步任務,2) 同步任務都在主線程上執行,形成一個執行棧,3)主線程之外,事件觸發線程管理着一個任務隊列,只要異步任務有了運行結果,就在任務隊列中放置一個事件,4)一旦執行棧中的所有同步任務執行完畢(此時 JavaScript 引擎空閒),系統就會讀取任務隊列,將可運行的異步任務添加到可執行棧中執行。而根據規範,事件循環是通過任務隊列的機制來進行協調的。
Bob: 事件循環是通過任務隊列的機制實現的,這是 how 的問題,還有 why 的問題呢, 爲什麼要有事件循環呢 ?
Alice: 是這樣的,JavaScript 是一個單線程的語言,也就是 從上到下 一行一行的執行代碼。但是這樣就有一個問題,如果遇到了某行 耗時很久的代碼就會一直 阻塞 在那,後面的代碼就無法執行了。比如說,下載一張圖片,發送一個 ajax 請求等等。在等待 圖片下載完成之前,ajax 請求成功之前,其實並沒有事情可做,與其這樣等着,還不如繼續執行後面的代碼。這就有了另一個問題,繼續執行後面的代碼之後,圖片下載完成之後對圖片的操作 該什麼時候執行呢,怎麼實現呢 ?
Bob: 你是說JavaScript 中的 ajax 請求,下載圖片之類的代碼可以看做是 異步任務,而其他順序執行的是 同步任務。異步任務中的代碼並不是立即執行的,而是依靠着事件循環 (有任務隊列實現的)來執行的。
Alice: 是的,維基百科中是這樣描述事件循環的: “Event Loop是一個程序結構,用於等待和發送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)”
上圖主線程的綠色部分,表示運行時間,橙色部分表示空閒時間。每當遇到I/O的時候,主線程就讓Event Loop線程去通知相應的I/O程序,然後接着往後運行,所以不存在等待時間。等到I/O程序完成操作,Event Loop線程再把結果返回主線程。主線程就調用事先設定的回調函數,完成整個任務。
虛線框住的是 JS 引擎,然後 stack 就是執行棧,執行棧中的代碼在運行過程中產生的 異步操作會在 操作完成後在 任務隊列中註冊回調函數。當執行棧爲空的時候,會從任務隊列中 取出隊頭的回調函數 放入執行棧中執行,如此循環往復,是爲事件循環。
用流程圖的方式描述事件循環。
Bob: 看完這幾張圖,大體上能夠理解事件循環了。事件循環是 JavaScript 中處理異步代碼的方式,正是有了 事件循環 JavaScript 纔是 非阻塞(Non-blocking)的。事件循環的具體實現就是,當主線程遇到異步代碼的時候,將異步代碼交給 消息線程 去處理,消息線程會在異步操作結束後,向事件隊列(任務隊列)中添加回調函數,當主線程的執行棧爲空的是,再從事件隊列中取出隊頭的回調函數放入 執行棧中執行。而事件循環所指的就是這個,循環往復的過程。
Alice: 😉你總結的很不錯嘛,What, Why, How 都有了。
Bob: 然後呢,微任務和宏任務在任務隊列中是怎麼樣的 ?微任務和微任務之間,宏任務和宏任務之間,微任務和宏任務之間的關係呢 ?
Alice: 還有一張圖很不錯,先看這個。
圖裏面 Task Queue 可以看出是任務隊列(宏任務)(事件隊列)Microtask Queue 就是微觀隊列,看到了吧,一個宏任務對應着一個微任務隊列。
這張圖也是這個意思,一個宏任務對應着若干個微任務組成的微任務隊列。
Bob: 額,微任務之間的執行順序是按照 微任務隊列中的順序 從隊頭到隊尾 ? 宏任務之間的執行順序就是按照 任務隊列 中的順序 從 隊頭到隊尾是嗎 ?
Alice 是的,宏任務主要有:script
(整體代碼)、setTimeout
、setInterval
、I/O
、UI交互事件
、postMessage
、MessageChannel
、setImmediate
(Node.js 環境)
微任務主要包含:Promise.then
、MutaionObserver
(與DOM有關)、process.nextTick
(Node.js 環境)。
然後運行的機制大概是這樣的:
- 執行一個宏任務(棧中沒有就從事件隊列中獲取)
- 執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中
- 宏任務執行完畢後,立即執行當前微任務隊列中的所有微任務(依次執行)
- 當前宏任務執行完畢,開始檢查渲染,然後GUI線程接管渲染
- 渲染完畢後,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)
Bob: 那 await 呢,await 後面的代碼算宏任務還是微任務呀 ?
Alice: 從字面意思上看await就是等待,await 等待的是一個表達式,這個表達式的返回值可以是一個promise對象也可以是其他值。很多人以爲await會一直等待之後的表達式執行完之後纔會繼續執行後面的代碼,實際上await是一個讓出線程的標誌。await後面的表達式會先執行一遍,將await後面的代碼加入到microtask中,然後就會跳出整個async函數來執行後面的代碼。
Bob:await 這樣子好像還是回調函數那一套呀,只不過是同步代碼的寫法。
Alice: 對啊,現在我們就可以 重新分析一下上面那道題了。
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('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
在這一段代碼中首先定義了兩個 async 函數 async1, async2, 然後執行了 console.log('script start')
,所以控制檯肯定是 {script start}。然後接着往下走,遇到一個 setTimeout ,放到任務隊列中【setTimeout1, 】。然後執行了 async1()
async1 是一個函數,執行第一行 輸出 'async1 start'
此時控制檯是{script start, async1 start, } 然後第二行 有 await ,await 會把 async2 執行了,輸出async2,此時控制檯是{script start, async1 start, async2} 然後把 console.log('async1 end');
放入微任務隊列【async1 end,】。這樣 async1 就執行完了。接着往下後, Promise 中的代碼立即執行,輸出 promise1, 此時控制檯是{script start, async1 start, async2, promise1}。然後 then 裏面的代碼被放到 微任務隊列中 【async1 end, promise2】,然後執行最後一句,控制檯中變成 {script start, async1 start, async2, promise1,script end,}。到現在 任務隊列中有 【setTimeout】微任務隊列中有 【async1 end, promise2】 。然後依次執行微任務對應的代碼,控制檯中的輸出應該變成{script start, async1 start, async2, promise1,script end, async1 end, promise2}。然後這個宏任務就結束了,開始下一個宏任務,輸出 setTimeout 就結束了。最終的結果就是{script start, async1 start, async2, promise1,script end, async1 end, promise2, setTimeout}
Bob: 醍醐灌頂,恍然大悟呀。
Alice: JavaScript 中的事件循環大概就是這樣了,不過上面的描述中還有很多不規範的地方,以後再來完善吧。
Bob:I’ll be the roundabout…
易錯點:
script start
async1 start
async2 // 誤認爲await 會阻塞 後面的代碼
async1 end
setTimeOut // 不知道script 本身就是一個宏任務
promise1
promise2 // 不知道 then 中的代碼會被添加到微任務
script end
總結:
舉一反三,再來點訓練吧。
Promise.resolve().then(function promise1 () {
console.log('promise1');
})
setTimeout(function setTimeout1 (){
console.log('setTimeout1')
Promise.resolve().then(function promise2 () {
console.log('promise2');
})
}, 0)
setTimeout(function setTimeout2 (){
console.log('setTimeout2')
}, 0)
// 執行結果 promise1 setTimeout1 promise2 setTimeout2
setTimeout(function () {
console.log(1);
});
new Promise(function(resolve,reject){
console.log(5)
resolve(2)
}).then(function(val){
console.log(val);
})
// 執行結果 5 2 1
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')
})
})
// 執行結果 1768 2435 9 11 10 12
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
//async2做出如下更改:
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise3');
resolve();
}).then(function() {
console.log('promise4');
});
console.log('script end');
// 執行結果
/*
script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout
*/
async function async1() {
console.log('async1 start');
await async2();
//更改如下:
setTimeout(function() {
console.log('setTimeout1')
},0)
}
async function async2() {
//更改如下:
setTimeout(function() {
console.log('setTimeout2')
},0)
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout3');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
/*執行結果
script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1
*/
async function a1 () {
console.log('a1 start')
await a2()
console.log('a1 end')
}
async function a2 () {
console.log('a2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
Promise.resolve().then(() => {
console.log('promise1')
})
a1()
let promise2 = new Promise((resolve) => {
resolve('promise2.then')
console.log('promise2')
})
promise2.then((res) => {
console.log(res)
Promise.resolve().then(() => {
console.log('promise3')
})
})
console.log('script end')
/*
運行結果
script start
a1 start
a2
promise2
script end
promise1
a1 end
promise2.then
promise3
setTimeout
*/
參考文獻:
- 阮一峯-什麼是event loop
- 這一次,徹底弄懂 JavaScript 執行機制
- 詳解JavaScript中的Event Loop(事件循環)機制
- 瀏覽器事件循環機制(event loop)
- 從一道題淺說 JavaScript 的事件循環
- js的事件循環是什麼
- JavaScript執行(一):Promise裏的代碼爲什麼比setTimeout先執行?
- 深入理解javascript中的事件循環event-loop