Event Loop 事件循環

說到 event loop,不得不提的是: JavaScript 是單線程的。即一次只能作一件事情。Event Loop 就是讓 JavaScript 在運行時具備併發的能力。javascript

1. 同步任務與異步任務

// global sync task

function add(a, b) {
  return a + b;
}

// add sync task
const sum = add();

// bar async task
setTimeout(function bar() {
  console.log('bar called!');
}, 1000);

// fetchUsers async task
fetch('https://www.example.com/api/v1/users').then(function fetchUsers(res) {
  console.log({ res });
});
  • 上面的腳本中包含同步任務有 global taskfoo task,包含的異步任務有 bar task
  • 同步:函數被調用時可以當即獲得結果
  • 異步:函數被調用時,不能當即獲得返回值,而是在未來的某個時刻獲得返回值

2. 執行棧

要想理解 Event Loop,就得先了解 JavaScript 中的 執行棧(JavaScript execution context stack)java

JavaScript 是單線程的,只有一個主線程。在這個主線程中,有一個棧。git

在每一個函數 運行時,會生成一個 執行上下文(execution context),這個執行上下文包含了當前函數運行時的參數,局部變量等信息。github

開始執行函數 時,執行上下文會被 推到執行棧中函數執行完畢後,這個 執行上下文又會從棧中彈出api

例子promise

function foo() {
  console.log('foo');
}

function bar() {
  console.log('bar');
  foo();
}

bar();
  • 以下圖所示

event-loop

  • 入執行棧:
  • 整個腳本開始運行,global 執行上下文 被推入 執行棧
  • bar() 函數開始運行,bar() 函數執行上下文 被推入 執行棧
  • foo() 函數開始運行,foo() 函數執行上下文 被推入 執行棧
  • 出執行棧:
  • foo() 函數執行完畢,彈出
  • bar() 函數執行完畢,彈出

總結:執行棧 至關於一條流水線,全部的 同步任務 運行時,都會被放到這條流水線上去一個一個 按照順序(函數調用順序) 執行。網絡

既然 同步任務運行時 是被放到 執行棧 中,那 異步任務運行時以及定義時 是被放到哪裏呢?併發

2. macrotask 和 microtask

在異步任務中,又分爲兩類任務:異步

  • macrotask:宏任務
  • microtask:微任務

要回答上面這個問題,首先了解一下 macrotaskmicrotaskasync

2.1. macrotask 宏任務

如下派發的任務就是 macrotask

  • setTimeout
  • setInterval
  • setImmediate
  • I/O(如:網絡請求)
  • UI rendering

每個 Event Loop 都有一個 macrotask 隊列

setTimeout(function foo() {
  console.log('foo');
}, 1000);

setTimeout(function bar() {
  console.log('foo');
}, 2000);
  • foo() macrotask 在 1000ms 後被推入 macrotask 隊列
  • bar() macrotask 在 2000ms 後被推入 macrotask 隊列

2.2. microtask 微任務

如下派發的任務時 microtask:

  • process.nextTick
  • promises
  • Object.observe
  • MutationObserver

每個 Event Loop 都有一個 microtask 隊列

// 調用 Promise 構造函數時,傳入的回調是會當即調用的,因此 foo 不算 microtask
// new Promise(function foo(resolve) {
//   console.log('foo');
//   resolve();
// });

// 調用 Promise 構造函數時,傳入的回調是會當即調用的,因此 bar 不算 microtask
// new Promise(function bar(resolve) {
//   console.log('bar');
//   resolve();
// });

Promise.resolve()
  .then(function foo() {
    console.log('promise1');
  })
  .then(function bar() {
    console.log('promise2');
  });
  • foo() microtask 被推入 microtask 隊列
  • bar() microtask 被推入 microtask 隊列

因此,如今來看看上面的問題:

  • 異步任務 運行時 以及 定義時 是被放到哪裏呢?
    答:
  • 若是 異步任務 屬於 macrotask,則在 定義時 被放到 macrotask 隊列 中;若是 異步任務 屬於 microtask,則在 定義時 被放到 microtask 隊列

上面只回答了 異步任務 定義時 被放置的位置。接下來經過 Event Loop 來回答 異步任務運行時 會在哪裏。

3. Event Loop

上面講了:

  • 同步任務:同步任務在 代碼層面,存在 定義狀態執行狀態。在 執行狀態 時被推入 執行棧 中執行
  • 異步任務:異步任務在 代碼層面,只存在 定義狀態。異步任務在 定義時,根據異步任務的類型,被推入不一樣的隊列中

    • macrotask 宏任務
    • microtask 微任務
  • 執行棧:全部的任務都會在 運行時 被推入到 執行棧 中執行

但上面還留下了一個問題:異步任務運行時 會在哪裏?

這個時候,就須要知道 Event Loop 了。

Event Loop 翻譯成中文爲 事件循環循環 二字,就說明了 Event Loop 表示是一個在不斷 循環 的過程。

這個 循環 包含了如下過程:

  1. 檢查 執行棧 中的全部任務是否所有執行完畢了。若未執行完,則繼續執行;不然進入下一步
  2. 檢查 microtask 隊列 是否存在 microtask。若存在 microtask,則將 microtask 隊列 中的全部 microtask 都推入 執行棧 中執行(不然進入下一步)。執行完畢後,進入下一步
  3. 檢查 macrotask 隊列 中是否存在 macrotask。若存在 macrotask,則將 macrotask 隊列 中的 第一個 出隊,推入到 執行棧 中執行(不然進入第 1 步,一個 Event Loop 完成)。執行完畢後,進入第 1 步(一個 Event Loop 完成)

event-loop2

  • 一個 Event Loop,包含了上面三個步驟

因此,如今能夠回答上面的問題了:異步任務運行時 會通過 Event Loop,且被推入到 執行棧 中。

4. 總結

Event Loop(事件循環)就是一個對 異步任務 進行 協調的過程

5. 參考

相關文章
相關標籤/搜索