深刻理解 JavaScript 事件循環,宏任務與微任務

前言

我以前寫了一篇《怎麼理解JavaScript異步機制的"詭異"》,初探了整個異步機制,但實際上真正在運行的時候,事件循環 EventLoop 並不僅是這麼簡單,這篇文章嘗試深刻理解 JavaScript 事件循環。javascript

然而到底什麼是事件循環?html

先跳出了 JavaScript 的範疇,先來看看更抽象的東西。java

事件驅動

要理解 事件循環(EventLoop) 首先先來理解,什麼是事件驅動,我這裏直接拿維基百科的解釋:事件驅動程序設計react

實際上 事件驅動(Event Driven) 普遍應用於GUI開發和異步IO開發,他也並非什麼特別的東西,能夠自行谷歌。c++

在事件驅動機制裏面,主線程都會運行一個事件循環(EventLoop),有時候也叫 消息循環(MessageLoop) 或者 運行循環(RunLoop),顧名思義它就是一個循環,仍是一個無限循環。web

事件驅動的機制,一般還有事件或者消息隊列,裏面放着各類要處理的事件或者消息,經過這個循環不斷的處理這些事件或者消息。面試

因此事件循環的背後就是事件驅動,而事件驅動是一種程序設計模型。shell

Chromium 的消息循環

瀏覽器的話我選用了 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)

從Chrome源碼看事件循環

其餘兩種消息也是分別在不一樣的 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 相關的消息
  1. 與平臺無關的消息,例如 setTimeout 的定時器就是屬於這個
  2. Chromium 的 IO 操做是基於 libevent 實現,它自己也是一個事件驅動的庫
  3. 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,任務有五種:

  • FinalizationGroupCleanupJobTask
  • CallbackTask
  • CallableTask
  • PromiseReactionJobTask
  • PromiseResolveThenableJobTask
  1. 是個內存回收的清理任務,使用過 Java 的童鞋應該都很熟悉,只是在 JavaScript 這是V8內部調用的
  2. 就是普通的回調,MutationObserver 也是這一類
  3. Callable
  4. 包括 FullfiledRejected 也就是 Promise 的完成和失敗
  5. 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

參照文章

  1. 併發模型與事件循環

  2. 理解WebKit和Chromium: 消息循環(Message Loop)

  3. 從Chrome源碼看事件循環

相關文章
相關標籤/搜索