我以前寫了一篇《怎麼理解JavaScript異步機制的"詭異"》,初探了整個異步機制,但實際上真正在運行的時候,事件循環 EventLoop
並不僅是這麼簡單,這篇文章嘗試深刻理解 JavaScript 事件循環。javascript
然而到底什麼是事件循環?html
先跳出了 JavaScript 的範疇,先來看看更抽象的東西。java
要理解 事件循環(EventLoop)
首先先來理解,什麼是事件驅動,我這裏直接拿維基百科的解釋:事件驅動程序設計。react
實際上 事件驅動(Event Driven)
普遍應用於GUI開發和異步IO開發,他也並非什麼特別的東西,能夠自行谷歌。c++
在事件驅動機制裏面,主線程都會運行一個事件循環(EventLoop)
,有時候也叫 消息循環(MessageLoop)
或者 運行循環(RunLoop)
,顧名思義它就是一個循環,仍是一個無限循環。web
事件驅動的機制,一般還有事件或者消息隊列,裏面放着各類要處理的事件或者消息,經過這個循環不斷的處理這些事件或者消息。面試
因此事件循環的背後就是事件驅動,而事件驅動是一種程序設計模型。shell
瀏覽器的話我選用了 Chromium
來舉例,它對事件驅動
的實現,其實叫 消息循環(MessageLoop)
。api
來看看 Chromium
處理自定義消息循環的源代碼 message_pump_default.ccpromise
void MessagePumpDefault::Run(Delegate* delegate) {
AutoReset<bool> auto_reset_keep_running(&keep_running_, true);
for (;;) { // 這裏是個死循環
#if defined(OS_MACOSX)
mac::ScopedNSAutoreleasePool autorelease_pool;
#endif
Delegate::NextWorkInfo next_work_info = delegate->DoSomeWork();
bool has_more_immediate_work = next_work_info.is_immediate();
if (!keep_running_)
break;
if (has_more_immediate_work)
continue;
has_more_immediate_work = delegate->DoIdleWork();
if (!keep_running_)
break;
if (has_more_immediate_work)
continue;
if (next_work_info.delayed_run_time.is_max()) {
event_.Wait();
} else {
event_.TimedWait(next_work_info.remaining_delay());
}
// Since event_ is auto-reset, we don't need to do anything special here
// other than service each delegate method.
}
}
複製代碼
其餘看不懂沒關係,只要看出來它是個無限循環便可,Chromium
在這個無限循環裏面,不斷從隊列裏面取出消息並處理,有興趣的童鞋能夠點擊下面的文章,擴展閱讀:
理解WebKit和Chromium: 消息循環(Message Loop)
其餘兩種消息也是分別在不一樣的 MessageLoop
中處理。
能夠看到事件循環自己的實現是經過它的宿主 Host
去實現的,因此在不一樣的環境,例如 Node 甚至是不一樣的瀏覽器,事件循環的機制實現都不同。
而我前面說到,事件驅動自己就是一種程序設計模型,實際上,瀏覽器上的事件循環,是有標準的,whatwg HTML5 有關 Event Loop Processing model
的章節就有詳細的介紹事件循環的標準規範。
而 JavaScript 設計的巧妙之處是,JavaScript 的引擎,自己並不實現事件循環的機制,這也是爲何 Node 可使用 V8
的緣由。
在 HTML5 規範裏面,有詳細的介紹 宏任務 Macrotasks
與 微任務 Microtasks
的實現規範。
實際上,之前是沒有,宏任務和微任務的概念的,用回以前的這張圖
這裏面 Web APIs
的異步操做都會經歷一次完整的事件循環,可是,有的時候,一些異步操做並不想要經歷整個事件循環,所以微任務就出現了,能夠查看下面的表分類。
任務 | Chrome | Node | 分類 |
---|---|---|---|
I/O | √ | √ | Marco |
requestAnimationFrame | √ | × | Marco |
setTimeout | √ | √ | Marco |
setInterval | √ | √ | Marco |
setImmediate | × | √ | Marco |
process.nextTick | × | √ | Micro |
MutationObserver | √ | × | Micro |
Promise | √ | √ | Micro |
在這一篇文章裏面,有介紹道 微任務 Microtasks
的概念。
Microtasks are usually scheduled for things that should happen straight after the currently executing script, such as reacting to a batch of actions, or to make something async without taking the penalty of a whole new task.
對上面的進行解釋的話,微任務能夠用來調度,那些當且僅當執行腳本後立馬執行的任務。例如:馬上對一系列的動做作出迴應,或者是不想承擔一次完整異步隊列代價的異步任務。
宏任務的本質就是:參與了事件循環的任務。
回到 Chromium
中,須要處理的消息主要分紅了三類:
Chromium
自定義消息Socket
或者文件等 IO 消息UI
相關的消息setTimeout
的定時器就是屬於這個Chromium
的 IO 操做是基於 libevent
實現,它自己也是一個事件驅動的庫UI
相關的其實屬於 blink
渲染引擎過來的消息,例如各類 DOM 的事件這些消息的具體任務又分紅了不少不少種,具體能夠參照源碼 task_type.h
下面是其中一部分任務
......
kJavascriptTimer = 10,
kRemoteEvent = 11,
kWebSocket = 12,
kPostedMessage = 13,
kUnshippedPortMessage = 14,
kFileReading = 15,
kDatabaseAccess = 16,
kPresentation = 17,
kSensor = 18,
....
複製代碼
這些任務都是參與了完整的事件循環,其實與 JavaScript 的引擎無關,都是在 Chromium
實現的。
微任務的本質:直接在 Javascript 引擎中的執行的,沒有參與事件循環的任務。
微任務其實是真實的隊列,具體的實現其實在 v8
的源碼裏面有 microtask.h,任務有五種:
V8
內部調用的MutationObserver
也是這一類Fullfiled
和 Rejected
也就是 Promise 的完成和失敗Thenable
對象的處理任務前面說了宏任務和微任務的概念,用一個源代碼來體現:
console.log('Start');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('End');
複製代碼
輸出的順序是:
Script Start
Script End
promise
setTimeout
複製代碼
promise
是在當前腳本代碼執行完後,馬上執行的,它並無參與事件循環,因此它的優先級是高於 setTimeout
。
由於這段代碼是在瀏覽器環境 Chrome 下面執行的,在不一樣的地方,因爲 EventLoop 實現不同,也會有不同的結果。
所以能夠這麼簡單的總結:
宏任務 Macrotasks
就是參與了事件循環的異步任務。微任務 Microtasks
就是沒有參與事件循環的「異步」任務。注意,微任務的「異步」,我是打了雙引號的,根據我以前的證實,能夠參照《怎麼理解JavaScript異步機制的"詭異"》,宏任務的主要工做是在別的線程裏面完成,完成後回調在主線程完成,但微任務實際上,它並無在別的線程上執行,它只是在當前 JavaScript 代碼執行完後馬上執行的,以此實現異步。
$(window).click(() => {
console.log('clicked1');
Promise.resolve().then(() => console.log('clicked promise1'));
setTimeout(() => console.log('clicked timeout1'), 0);
});
$(window).click(() => {
setTimeout(() => {
console.log('clicked2');
Promise.resolve().then(() => console.log('clicked promise2'));
setTimeout(() => console.log('clicked timeout2'), 0);
}, 0);
});
$(window).click(() => {
Promise.resolve().then(() => {
console.log('clicked3');
Promise.resolve().then(() => console.log('clicked promise3'));
setTimeout(() => console.log('clicked timeout3'), 0);
});
});
複製代碼
這個例子是我本身寫的,而這三個事件的順序變化,也會對最後結果的順序有不一樣的影響,能夠加深你的瞭解。
除了面試之外,瞭解這些東西對實際開發真的會有好處嗎?或者說花時間去了解這些東西真的划算嗎?
答案是顯然的。
瞭解 JavaScript 的底層運行機制,會讓你有意識的在開發中避免掉一些坑,或者讓你排查問題的時候,你更有方向性。
至少要理解,爲何人們常說,不要阻塞事件循環 Don't break the EventLoop,得有清晰的認識。
另外強烈建議你觀看,下面這個 youtube 的視頻,看完之後你會對事件循環有了很是深入的體會。
What the heck is the event loop anyway? - Philip Roberts