詳解JavaScript事件循環(huán)機(jī)制
眾所周知,JavaScript 是一門(mén)單線(xiàn)程語(yǔ)言,雖然在 html5 中提出了 Web-Worker ,但這并未改變 JavaScript 是單線(xiàn)程這一核心??煽碒TML規(guī)范中的這段話(huà):
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.
為了協(xié)調(diào)事件、用戶(hù)交互、腳本、UI 渲染和網(wǎng)絡(luò)處理等行為,用戶(hù)引擎必須使用 event loops。Event Loop 包含兩類(lèi):一類(lèi)是基于 Browsing Context ,一種是基于 Worker ,二者是獨(dú)立運(yùn)行的。 下面本文用一個(gè)例子,著重講解下基于 Browsing Context 的事件循環(huán)機(jī)制。
來(lái)看下面這段 JavaScript 代碼:
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');
先猜測(cè)一下這段代碼的輸出順序是什么,再去瀏覽器控制臺(tái)輸入一下,看看實(shí)際輸出的順序和你猜測(cè)出的順序是否一致,如果一致,那就說(shuō)明,你對(duì) JavaScript 的事件循環(huán)機(jī)制還是有一定了解的,繼續(xù)往下看可以鞏固下你的知識(shí);而如果實(shí)際輸出的順序和你的猜測(cè)不一致,那么本文下面的部分會(huì)為你答疑解惑。
任務(wù)隊(duì)列
所有的任務(wù)可以分為同步任務(wù)和異步任務(wù),同步任務(wù),顧名思義,就是立即執(zhí)行的任務(wù),同步任務(wù)一般會(huì)直接進(jìn)入到主線(xiàn)程中執(zhí)行;而異步任務(wù),就是異步執(zhí)行的任務(wù),比如ajax網(wǎng)絡(luò)請(qǐng)求,setTimeout 定時(shí)函數(shù)等都屬于異步任務(wù),異步任務(wù)會(huì)通過(guò)任務(wù)隊(duì)列( Event Queue )的機(jī)制來(lái)進(jìn)行協(xié)調(diào)。具體的可以用下面的圖來(lái)大致說(shuō)明一下:

同步和異步任務(wù)分別進(jìn)入不同的執(zhí)行環(huán)境,同步的進(jìn)入主線(xiàn)程,即主執(zhí)行棧,異步的進(jìn)入 Event Queue 。主線(xiàn)程內(nèi)的任務(wù)執(zhí)行完畢為空,會(huì)去 Event Queue 讀取對(duì)應(yīng)的任務(wù),推入主線(xiàn)程執(zhí)行。 上述過(guò)程的不斷重復(fù)就是我們說(shuō)的 Event Loop (事件循環(huán))。
在事件循環(huán)中,每進(jìn)行一次循環(huán)操作稱(chēng)為tick,通過(guò)閱讀規(guī)范可知,每一次 tick 的任務(wù)處理模型是比較復(fù)雜的,其關(guān)鍵的步驟可以總結(jié)如下:
- 在此次 tick 中選擇最先進(jìn)入隊(duì)列的任務(wù)( oldest task ),如果有則執(zhí)行(一次)
- 檢查是否存在 Microtasks ,如果存在則不停地執(zhí)行,直至清空Microtask Queue
- 更新 render
主線(xiàn)程重復(fù)執(zhí)行上述步驟
可以用一張圖來(lái)說(shuō)明下流程:

這里相信有人會(huì)想問(wèn),什么是 microtasks ?規(guī)范中規(guī)定,task分為兩大類(lèi), 分別是 Macro Task (宏任務(wù))和 Micro Task(微任務(wù)), 并且每個(gè)宏任務(wù)結(jié)束后, 都要清空所有的微任務(wù),這里的 Macro Task也是我們常說(shuō)的 task ,有些文章并沒(méi)有對(duì)其做區(qū)分,后面文章中所提及的task皆看做宏任務(wù)( macro task)。
(macro)task 主要包含:script( 整體代碼)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 環(huán)境)
microtask主要包含:Promise、MutaionObserver、process.nextTick(Node.js 環(huán)境)
setTimeout/Promise 等API便是任務(wù)源,而進(jìn)入任務(wù)隊(duì)列的是由他們指定的具體執(zhí)行任務(wù)。來(lái)自不同任務(wù)源的任務(wù)會(huì)進(jìn)入到不同的任務(wù)隊(duì)列。其中 setTimeout 與 setInterval 是同源的。
分析示例代碼
千言萬(wàn)語(yǔ),不如就著例子講來(lái)的清楚。下面我們可以按照規(guī)范,一步步執(zhí)行解析下上面的例子,先貼一下例子代碼(免得你往上翻)。
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');
整體 script 作為第一個(gè)宏任務(wù)進(jìn)入主線(xiàn)程,遇到 console.log,輸出 script start
- 遇到 setTimeout,其回調(diào)函數(shù)被分發(fā)到宏任務(wù) Event Queue 中
- 遇到 Promise,其 then函數(shù)被分到到微任務(wù) Event Queue 中,記為 then1,之后又遇到了 then 函數(shù),將其分到微任務(wù) Event Queue 中,記為 then2
- 遇到 console.log,輸出 script end
至此,Event Queue 中存在三個(gè)任務(wù),如下表:
| 宏任務(wù) | 微任務(wù) |
|---|---|
| setTimeout | then1 |
| - | then2 |
- 執(zhí)行微任務(wù),首先執(zhí)行then1,輸出 promise1, 然后執(zhí)行 then2,輸出 promise2,這樣就清空了所有微任務(wù)
- 執(zhí)行 setTimeout 任務(wù),輸出 setTimeout 至此,輸出的順序是:script start, script end, promise1, promise2, setTimeout
so,你猜對(duì)了嗎?
看看你掌握了沒(méi)
再來(lái)一個(gè)題目,來(lái)做個(gè)練習(xí):
console.log('script start');
setTimeout(function() {
console.log('timeout1');
}, 10);
new Promise(resolve => {
console.log('promise1');
resolve();
setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
console.log('then1')
})
console.log('script end');
這個(gè)題目就稍微有點(diǎn)復(fù)雜了,我們?cè)俜治鱿拢?/p>
首先,事件循環(huán)從宏任務(wù) (macrotask) 隊(duì)列開(kāi)始,最初始,宏任務(wù)隊(duì)列中,只有一個(gè) scrip t(整體代碼)任務(wù);當(dāng)遇到任務(wù)源 (task source) 時(shí),則會(huì)先分發(fā)任務(wù)到對(duì)應(yīng)的任務(wù)隊(duì)列中去。所以,就和上面例子類(lèi)似,首先遇到了console.log,輸出 script start; 接著往下走,遇到 setTimeout 任務(wù)源,將其分發(fā)到任務(wù)隊(duì)列中去,記為 timeout1; 接著遇到 promise,new promise 中的代碼立即執(zhí)行,輸出 promise1, 然后執(zhí)行 resolve ,遇到 setTimeout ,將其分發(fā)到任務(wù)隊(duì)列中去,記為 timemout2, 將其 then 分發(fā)到微任務(wù)隊(duì)列中去,記為 then1; 接著遇到 console.log 代碼,直接輸出 script end 接著檢查微任務(wù)隊(duì)列,發(fā)現(xiàn)有個(gè) then1 微任務(wù),執(zhí)行,輸出then1 再檢查微任務(wù)隊(duì)列,發(fā)現(xiàn)已經(jīng)清空,則開(kāi)始檢查宏任務(wù)隊(duì)列,執(zhí)行 timeout1,輸出 timeout1; 接著執(zhí)行 timeout2,輸出 timeout2 至此,所有的都隊(duì)列都已清空,執(zhí)行完畢。其輸出的順序依次是:script start, promise1, script end, then1, timeout1, timeout2
用流程圖看更清晰:

總結(jié)
有個(gè)小 tip:從規(guī)范來(lái)看,microtask 優(yōu)先于 task 執(zhí)行,所以如果有需要優(yōu)先執(zhí)行的邏輯,放入microtask 隊(duì)列會(huì)比 task 更早的被執(zhí)行。
最后的最后,記住,JavaScript 是一門(mén)單線(xiàn)程語(yǔ)言,異步操作都是放到事件循環(huán)隊(duì)列里面,等待主執(zhí)行棧來(lái)執(zhí)行的,并沒(méi)有專(zhuān)門(mén)的異步執(zhí)行線(xiàn)程。。
相關(guān)文章
Javascript獲取窗口(容器)的大小及位置參數(shù)列舉及簡(jiǎn)要說(shuō)明
Javascript獲取窗口(容器)的大小及位置一系列的東西比較多,容易混淆,在這里列舉及簡(jiǎn)要說(shuō)明下,需要的朋友可以參考下2012-12-12
javascript處理a標(biāo)簽超鏈接默認(rèn)事件的方法
這篇文章主要介紹了javascript處理a標(biāo)簽超鏈接默認(rèn)事件的方法,涉及event.preventDefault()方法阻止默認(rèn)事件的使用技巧,需要的朋友可以參考下2015-06-06
window.close(); 關(guān)閉瀏覽器窗口js代碼的總結(jié)介紹
下面小編就為大家?guī)?lái)一篇window.close(); 關(guān)閉瀏覽器窗口js代碼的總結(jié)介紹。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-07-07
uni-app自定義組件components導(dǎo)入失敗或頁(yè)面不顯示文本等解決方法
這篇文章主要給大家介紹了關(guān)于uni-app自定義組件components導(dǎo)入失敗或頁(yè)面不顯示文本等的解決方法,眾所周知Uni-app支持使用自定義組件,文中通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-08-08
關(guān)于JavaScript的Array數(shù)組方法詳解
這篇文章主要介紹了關(guān)于JavaScript的Array數(shù)組方法詳解,數(shù)組是一個(gè)固定長(zhǎng)度的存儲(chǔ)相同數(shù)據(jù)類(lèi)型的數(shù)據(jù)結(jié)構(gòu),數(shù)組中的元素被存儲(chǔ)在一段連續(xù)的內(nèi)存空間中,它是最簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu)之一,需要的朋友可以參考下2023-05-05
詳解JavaScript中的客戶(hù)端消息框架設(shè)計(jì)原理
這篇文章主要介紹了詳解JavaScript中的客戶(hù)端消息框架設(shè)計(jì)原理,包括客戶(hù)端和服務(wù)器端的通信等方面的內(nèi)容,需要的朋友可以參考下2015-06-06
簡(jiǎn)單的ajax連接庫(kù)分享(不用jquery的ajax)
自己寫(xiě)了一個(gè)簡(jiǎn)單的ajax連接庫(kù),沒(méi)有使用jquery的ajax,這個(gè)代碼要比jquery輕量,大家參考使用吧,2014-01-01

