javascript:為什麼Promises比setTimeout()更快?

語言: CN / TW / HK

1.實驗

我們來做個實驗。什麼執行得更快:立即解決的承諾或立即超時(又稱超時0 millisecond)?

Promise.resolve(1).then(function resolve() { console.log('Resolved!'); }); setTimeout(function timeout() { console.log('Timed out!'); }, 0); // logs 'Resolved!' // logs 'Timed out!' Promise.resolve(1)是一個靜態函數,它返回一個立即解決的承諾。setTimeout(callback, 0) ,執行回調,延遲時間為0 millisecond。

打開演示並檢查控制枱。你會注意到'Resolved!' 先被記錄下來,然後是'Timeout completed!' 。一個立即解決的承諾比一個立即超時的承諾處理得更快。

可能是因為Promise.resolve(true).then(...) 是在setTimeout(..., 0) 之前調用的,所以承諾的處理速度更快? 這個問題很合理。

讓我們稍微改變一下實驗的條件,先調用setTimeout(..., 0)

setTimeout(function timeout() { console.log('Timed out!'); }, 0); Promise.resolve(1).then(function resolve() { console.log('Resolved!'); }); // logs 'Resolved!' // logs 'Timed out!' 打開演示,看看控制枱。嗯......同樣的結果!

實驗表明,一個立即解決的承諾會在立即超時之前被處理。最大的問題是...為什麼?

2.事件循環

與異步JavaScript有關的問題可以通過研究事件循環來回答。讓我們回顧一下異步JavaScript工作方式的主要組成部分。

注意:如果你對事件循環不熟悉,我建議在進一步閲讀之前先看這個視頻

Event Loop Empty

調用堆棧是一個LIFO(Last In, First Out)結構,用於存儲代碼執行過程中創建的執行環境。簡單地説,調用堆棧執行函數。

網絡API是異步操作(獲取請求、承諾、定時器)及其回調的地方,等待完成。

任務隊列(也叫宏任務)是一個FIFO(先進先出)結構,它保存着準備執行的異步操作的回調。例如,一個超時的setTimeout() 的回調--準備被執行--被排在任務隊列中。

任務隊列(也被稱為微任務)是一個FIFO(先進先出)結構,保存着準備執行的承諾的回調。例如,一個已履行的承諾的解析或拒絕回調被排在作業隊列中。

最後,事件循環永久地監視調用棧是否為空。如果調用棧是空的,事件循環會查看作業隊列或任務隊列,並將任何準備執行的回調排入調用棧。

3.工作隊列與任務隊列

讓我們再從事件循環的角度看一下這個實驗。我將對代碼的執行做一個逐步的分析。

A) 調用堆棧執行setTimeout(..., 0) ,並安排一個定時器。timeout() 回調被存儲在Web APIs中。

setTimeout(function timeout() { console.log('Timed out!'); }, 0); Promise.resolve(1).then(function resolve() { console.log('Resolved!'); });

Event Loop

B) 調用堆棧執行Promise.resolve(true).then(resolve) ,並安排了一個承諾解析。resolved() 回調存儲在Web APIs中。 setTimeout(function timeout() { console.log('Timed out!'); }, 0); Promise.resolve(1).then(function resolve() { console.log('Resolved!'); });

Event Loop

C)承諾立即被解決,同時計時器也立即超時。因此,定時器回調timeout()排到任務隊列中,承諾回調resolve()排到工作隊列中。

Event Loop

D) 現在是有趣的部分:事件循環優先於任務去排隊工作。事件循環從作業隊列中取消承諾回調resolve() ,並把它放到調用棧中。然後調用堆棧執行承諾回調resolve()setTimeout(function timeout() { console.log('Timed out!'); }, 0); Promise.resolve(1).then(function resolve() { console.log('Resolved!'); }); Event Loop

E) 最後,事件循環從任務隊列中的定時器回調timeout() ,並放入調用堆棧。然後調用堆棧執行定時器回調timeout()

setTimeout(function timeout() { console.log('Timed out!'); }, 0); Promise.resolve(1).then(function resolve() { console.log('Resolved!'); });

'Timed out!' 被記錄到控制枱。

Event Loop

調用堆棧是空的。腳本的執行已經完成。

4.總結

為什麼一個立即解決的承諾比一個立即的定時器處理得快?

因為事件循環優先從作業隊列(存儲已實現的承諾的回調)中去排隊作業,而不是從任務隊列(存儲已超時的setTimeout() 回調)中去排隊任務。