你是否曾經想過為什麼某些 JavaScript 程式碼似乎無法按順序運行?理解這一點的關鍵是 事件循環

JavaScript 的事件循環可能難以理解,特別是在處理不同類型的非同步操作時。在本文中,我們將解析 JavaScript 如何處理 同步非同步 程式碼、微任務巨任務,以及為什麼某些事情會以特定的順序發生。

內容表

  1. 同步和非同步程式碼
  2. 微任務和巨任務
  3. 事件循環
  4. 範例
  5. 結論

同步和非同步程式碼

JavaScript 主要以兩種方式處理操作:同步非同步。理解它們之間的區別是掌握 JavaScript 如何處理任務及如何編寫高效且非阻塞程式碼的關鍵。

什麼是同步程式碼?

同步程式碼是 JavaScript 的預設行為,意味著每一行程式碼依序運行。例如:

console.log("第一");
console.log("第二");

這將輸出:

第一
第二

什麼是非同步程式碼?

非同步程式碼允許某些任務在背景運行並稍後完成,而不會阻塞其餘程式碼。像 setTimeout()Promise 的函數都是非同步程式碼的範例。

這是一個使用 setTimeout() 的非同步程式碼簡單範例:

console.log("第一");

setTimeout(() => {
  console.log("第二");
}, 0);

console.log("第三");

這將輸出:

第一
第三
第二

JavaScript 中的非同步模式:

在 JavaScript 中,有幾種方法來處理非同步操作:

  1. 回呼函數: 作為另一個函數的引數傳入的函數,並在第一個函數完成其任務後執行。

程式碼範例:

console.log("開始");

function asyncTask(callback) {
  setTimeout(() => {
    console.log("非同步任務完成");
    callback();
  }, 2000);
}

asyncTask(() => {
  console.log("任務完成");
});

console.log("結束");
  1. Promise: Promise 代表一個未來的值(或錯誤),最終會由非同步函數返回。

程式碼範例:

console.log("開始");

const asyncTask = new Promise((resolve) => {
  setTimeout(() => {
    console.log("非同步任務完成");
    resolve();
  }, 2000);
});

asyncTask.then(() => {
  console.log("任務完成");
});

console.log("結束");
  1. Async/Await: Async/await 是建立在 Promise 上的語法糖,使我們可以編寫看起來像同步的非同步程式碼。

程式碼範例:

console.log("開始");

async function asyncTask() {
  await new Promise((resolve) => {
    setTimeout(() => {
      console.log("非同步任務完成");
      resolve();
    }, 2000);
  });

  console.log("任務完成");
}

asyncTask();

console.log("結束");

同步 vs 非同步程式碼

為了更好地理解 JavaScript 的這些執行方式及其之間的區別,以下是多個方面的詳細比較:

方面 同步程式碼 非同步程式碼
執行順序 按行序列執行 允許任務在背景運行,而其他程式碼繼續執行
性能 若涉及長時間任務,可能導致性能問題 在 I/O 密集型操作中性能更佳;防止瀏覽器環境中的 UI 凍結
程式碼複雜度 通常較簡單且易讀 可能更複雜,特別是在嵌套回呼函數的情況下(回呼地獄)
記憶體使用 若等待長時間操作可能使用更多記憶體 對於長時間任務通常更有效率
可擴展性 對於有許多並發操作的應用程序擴展性較差 對於處理多個同時操作的應用程序更具可擴展性

這個比較突顯了同步和非同步程式碼之間的關鍵區別,幫助開發者根據其特定用例和性能需求選擇合適的方法。


微任務和巨任務

在 JavaScript 中,微任務和巨任務是兩種排隊並在事件循環的不同部分執行的任務,這決定了 JavaScript 如何處理非同步操作。

微任務和巨任務都在事件循環中排隊和執行,但它們具有不同的優先順序和執行上下文。微任務在巨任務隊列中的下一個任務之前不斷處理,直到微任務隊列為空。而巨任務則在微任務隊列清空後執行,並在下一個事件循環周期開始之前執行。

什麼是微任務

微任務是指需要在當前操作完成後但在下一個事件循環周期開始之前執行的任務。微任務優先於巨任務,並在微任務隊列為空之前不斷處理。

微任務的範例:

  • Promise(當使用 .then().catch() 處理器時)
  • MutationObserver 回呼(用於觀察 DOM 的變化)
  • Node.js 中的某些 process.nextTick()

程式碼範例

console.log("開始");

Promise.resolve().then(() => {
  console.log("微任務");
});

console.log("結束");

輸出:

開始
結束
微任務

解釋:

  • 代碼首先輸出 "開始",這是同步的。
  • Promise 處理器(微任務)排入微任務。
  • 然後輸出 "結束"(同步),然後事件循環處理微任務,輸出 "微任務"。

什麼是巨任務

巨任務是在微任務隊列清空後執行的任務,並在下一個事件循環周期開始之前執行。這些任務代表如 I/O 或渲染等操作,通常在特定事件後或延遲後安排。

巨任務的範例:

  • setTimeout()
  • setInterval()
  • setImmediate()(在 Node.js 中)
  • I/O 回呼(檔案讀取/寫入)
  • UI 渲染任務(在瀏覽器中)

程式碼範例:

console.log("開始");

setTimeout(() => {
  console.log("巨任務");
}, 0);

console.log("結束");

輸出:

開始
結束
巨任務

解釋:

  • 代碼首先輸出 "開始",這是同步的。
  • setTimeout()(巨任務)被排入。
  • 這時輸出 "結束"(同步),然後事件循環處理巨任務,輸出 "巨任務"。

微任務 vs 巨任務

方面 微任務 巨任務
執行時間 在當前腳本之後立即執行,並在渲染之前 在下一個事件循環迭代中執行
隊列優先順序 優先級較高,在巨任務之前處理 優先級較低,在所有微任務完成後處理
範例 Promise、queueMicrotask()、MutationObserver setTimeout()、setInterval()、I/O 操作、UI 渲染
用例 需要盡快執行而不讓事件循環讓步的任務 可以延遲或不需要立即執行的任務

事件循環

事件循環是 JavaScript 中一個基本概念,能夠在 JavaScript 雖然是單執行緒的情況下啟用非阻塞的非同步操作。它負責處理非同步回呼並確保 JavaScript 在不被耗時操作阻塞的情況下平穩運作。

什麼是事件循環

事件循環是一種機制,它使 JavaScript 能夠有效地處理非同步操作。它不斷檢查呼叫堆疊和任務隊列(或微任務隊列),以確定下一個應該執行的函數。

若要更好地理解事件循環,了解 JavaScript 的內部運作是很重要的。需注意的是 JavaScript 是一種 單執行緒 語言,這意味著它一次只能做一件事情。只有一個呼叫堆疊,這個堆疊存儲待執行的函數。這使同步程式碼簡單,但對於像從伺服器獲取數據或設置計時器這樣的任務,會耗時間完成。沒有事件循環的話,JavaScript 將會一直等待這些任務,什麼都不會發生。

事件循環如何運作

1. 呼叫堆疊:

呼叫堆疊是當前正在執行的函數所在的位置。JavaScript 在處理程式碼時,不斷地將函數添加到堆疊,也不斷地將其從堆疊中刪除。

2. 非同步任務啟動:

當遇到像 setTimeout、fetch 或 Promise 之類的非同步任務時,JavaScript 將該任務委派給瀏覽器的 Web API(如計時器 API、網路 API 等),這些 API 在背景中處理該任務。

3. 任務移動到任務隊列:

一旦非同步任務完成(例如,計時器完成,或從伺服器接收到數據),回呼(處理結果的函數)會移動到任務隊列(或在 Promise 的情況下進入微任務隊列)。

4. 呼叫堆疊完成當前執行:

JavaScript 繼續執行同步程式碼。一旦呼叫堆疊空了,事件循環會從任務隊列(或微任務隊列)中取出第一個任務並將其放到呼叫堆疊中執行。

5. 重複:

這個過程重複進行。事件循環確保在當前同步任務完成後,所有的非同步任務都被處理。

範例

現在我們對事件循環的運作有了更好更清晰的理解,讓我們通過一些範例來鞏固我們的理解。

範例 1:使用 Promise 和事件循環的計時器

function exampleOne() {
  console.log("開始");

  setTimeout(() => {
    console.log("計時結束");
  }, 1000);

  Promise.resolve().then(() => {
    console.log("已解決");
  });

  console.log("結束");
}

exampleOne();

輸出:

開始
結束
已解決
計時結束

解釋:

  • 步驟 1: "開始" 被打印(同步)。
  • 步驟 2: setTimeout 安排在 1 秒後打印 "計時結束"(巨任務隊列)。
  • 步驟 3: 一個 Promise 被解決,"已解決" 的消息被推入微任務隊列。
  • 步驟 4: "結束" 被打印(同步)。
  • 步驟 5: 呼叫堆疊現在是空的,所以微任務隊列優先執行,打印"已解決"。
  • 步驟 6: 1 秒後,巨任務隊列執行,打印 "計時結束"。

範例 2:嵌套的 Promise 和計時器

function exampleTwo() {
  console.log("開始");

  setTimeout(() => {
    console.log("計時器 1");
  }, 0);

  Promise.resolve().then(() => {
    console.log("Promise 1 已解決");

    setTimeout(() => {
      console.log("計時器 2");
    }, 0);

    return Promise.resolve().then(() => {
      console.log("Promise 2 已解決");
    });
  });

  console.log("結束");
}

exampleTwo();

輸出:

開始
結束
Promise 1 已解決
Promise 2 已解決
計時器 1
計時器 2

解釋:

  • 步驟 1: "開始" 被打印(同步)。
  • 步驟 2: 第一個 setTimeout 安排在 0 毫秒後調用 "計時器 1"(巨任務隊列)。
  • 步驟 3: Promise 被解決,推送到微任務隊列。
  • 步驟 4: "結束" 被打印(同步)。
  • 步驟 5: 微任務隊列優先執行:
    • "Promise 1 已解決" 被打印。
    • "計時器 2" 被排入(巨任務隊列)。
    • 另一個 Promise 被解決,"Promise 2 已解決" 被打印。
  • 步驟 6: 隨後,處理巨任務隊列:
    • "計時器 1" 被打印。
    • "計時器 2" 被打印。

範例 3:混合同步和非同步操作

function exampleThree() {
  console.log("步驟 1:同步");

  setTimeout(() => {
    console.log("步驟 2:計時器 1");
  }, 0);

  Promise.resolve().then(() => {
    console.log("步驟 3:Promise 1 已解決");

    Promise.resolve().then(() => {
      console.log("步驟 4:Promise 2 已解決");
    });

    setTimeout(() => {
      console.log("步驟 5:計時器 2");
    }, 0);
  });

  setTimeout(() => {
    console.log(
      "步驟 6:立即(使用 setTimeout 設置 0 延遲作為後備)"
    );
  }, 0);

  console.log("步驟 7:同步結束");
}

exampleThree();

輸出:

步驟 1:同步
步驟 7:同步結束
步驟 3:Promise 1 已解決
步驟 4:Promise 2 已解決
步驟 2:計時器 1
步驟 6:立即(使用 setTimeout 設置 0 延遲作為後備)
步驟 5:計時器 2

解釋:

  • 步驟 1: "步驟 1:同步" 被打印(同步)。
  • 步驟 2: 第一個 setTimeout 排入 "步驟 2:計時器 1"(巨任務隊列)。
  • 步驟 3: Promise 被解決,調用 "步驟 3:Promise 1 已解決"(微任務隊列)。
  • 步驟 4: 另一個同步日誌,"步驟 7:同步結束" 被打印。
  • 步驟 5: 微任務隊列優先執行:
    • "步驟 3:Promise 1 已解決" 被打印。
    • "步驟 4:Promise 2 已解決" 被打印(嵌套的微任務)。
  • 步驟 6: 巨任務隊列被執行:
    • "步驟 2:計時器 1" 被打印。
    • "步驟 6:立即(使用 setTimeout 設置 0 延遲作為後備)" 被打印。
    • 最後,"步驟 5:計時器 2" 被打印。

結論

在 JavaScript 中,掌握同步和非同步操作,以及理解事件循環和它如何處理任務,是編寫高效和性能良好應用程序的關鍵。

  • 同步函數按序列執行,阻止後續程式碼在完成之前執行,而非同步函數(如 setTimeout 和 promises)允許非阻塞行為,使有效的多工操作成為可能。
  • 微任務(如 promises)擁有比巨任務(如 setTimeout)更高的優先級,這意味著事件循環在當前執行後立即處理微任務,然後再移至巨任務隊列。
  • 事件循環是使 JavaScript 處理非同步程式碼的核心機制,它管理任務的執行順序,確保在處理下一個隊列(微任務或巨任務)之前,呼叫堆疊清空。

提供的範例逐步說明了同步程式碼、 promises、計時器和事件循環之間的交互。理解這些概念是掌握 JavaScript 中非同步程式設計的關鍵,確保你的程式碼有效運行,避免常見的陷阱,如競賽條件或意外的執行順序。


保持更新和聯繫

為了確保你不會錯過本系列的任何部分,並與我聯繫,進行更深入的軟體開發(Web、伺服器、手機或抓取/自動化)討論、推播通知和其他令人興奮的技術主題,請關注我:

敬請期待並快樂編碼 👨‍💻🚀


原文出處:https://dev.to/emmanuelayinde/understanding-asynchronous-programming-in-javascript-synchronous-asynchronous-microtasks-macrotasks-and-the-event-loop-h5e


共有 0 則留言