首頁  >  文章  >  web前端  >  了解 JavaScript 中的非同步程式設計:事件循環初學者指南

了解 JavaScript 中的非同步程式設計:事件循環初學者指南

PHPz
PHPz原創
2024-09-11 06:32:03778瀏覽

Understanding Asynchronous Programming in JavaScript: Beginner

您有沒有想過為什麼某些 JavaScript 程式碼似乎運作不正常?理解這一點的關鍵是事件循環

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

目錄

  1. 同步和非同步程式碼
    • 什麼是同步程式碼
    • 什麼是非同步程式碼
    • JavaScript 中的非同步模式
    • 同步與非同步程式碼
  2. 微任務和巨集任務
    • 什麼是微任務
    • 什麼是宏任務
    • 微任務與巨集任務
  3. 事件循環
    • 什麼是事件循環
    • 事件循環如何運作
  4. 範例
    • 範例 1:具有 Promise 和事件循環的計時器
    • 範例 2:巢狀 Promise 和計時器
    • 範例 3:混合同步與非同步操作
  5. 結論

同步和非同步程式碼

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

什麼是同步程式碼?

JavaScript 中預設使用同步程式碼,這表示每一行都按順序依序運行。例如:

console.log("First");
console.log("Second");

這將輸出:

First
Second

什麼是異步代碼?

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

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

console.log("First");

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

console.log("Third");

這將輸出:

First
Third
Second

JavaScript 中的非同步模式:

JavaScript 中有幾種處理非同步操作的方法:

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

程式碼範例:

console.log("Start");

function asyncTask(callback) {
  setTimeout(() => {
    console.log("Async task completed");
    callback();
  }, 2000);
}

asyncTask(() => {
  console.log("Task finished");
});

console.log("End");
  1. Promises: Promise 代表非同步函數最終將傳回的未來值(或錯誤)。

程式碼範例:

console.log("Start");

const asyncTask = new Promise((resolve) => {
  setTimeout(() => {
    console.log("Async task completed");
    resolve();
  }, 2000);
});

asyncTask.then(() => {
  console.log("Task finished");
});

console.log("End");
  1. Async/Await: Async/await 是建構在 Promise 之上的語法糖,允許我們編寫看起來同步的非同步程式碼。

程式碼範例:

console.log("Start");

async function asyncTask() {
  await new Promise((resolve) => {
    setTimeout(() => {
      console.log("Async task completed");
      resolve();
    }, 2000);
  });

  console.log("Task finished");
}

asyncTask();

console.log("End");

同步與非同步程式碼

為了更好地理解 javascript 的每種執行方法以及它們之間的差異,這裡詳細介紹了 javascript 函數的多個方面的差異。

Aspect Synchronous Code Asynchronous Code
Execution Order Executes line by line in a sequential manner Allows tasks to run in the background while other code continues to execute
Performance Can lead to performance issues if long-running tasks are involved Better performance for I/O-bound operations; prevents UI freezing in browser environments
Code Complexity Generally simpler and easier to read Can be more complex, especially with nested callbacks (callback hell)
Memory Usage May use more memory if waiting for long operations Generally more memory-efficient for long-running tasks
Scalability Less scalable for applications with many concurrent operations More scalable, especially for applications handling multiple simultaneous operations

This comparison highlights the key differences between synchronous and asynchronous code, helping developers choose the appropriate approach based on their specific use case and performance requirements.


Microtasks and Macrotasks

In JavaScript, microtasks and macrotasks are two types of tasks that are queued and executed in different parts of the event loop, which determines how JavaScript handles asynchronous operations.

Microtasks and macrotasks are both queued and executed in the event loop, but they have different priorities and execution contexts. Microtasks are processed continuously until the microtask queue is empty before moving on to the next task in the macrotask queue. Macrotasks, on the other hand, are executed after the microtask queue has been emptied and before the next event loop cycle starts.

What are Microtasks

Microtasks are tasks that need to be executed after the current operation completes but before the next event loop cycle starts. Microtasks get priority over macrotasks and are processed continuously until the microtask queue is empty before moving on to the next task in the macrotask queue.

Examples of microtasks:

  • Promises (when using .then() or .catch() handlers)
  • MutationObserver callbacks (used to observe changes to the DOM)
  • Some process.nextTick() in Node.js

Code Sample

console.log("Start");

Promise.resolve().then(() => {
  console.log("Microtask");
});

console.log("End");

Output:

Start
End
Microtask

Explanation:

  • The code first logs "Start", which is synchronous.
  • The promise handler (Microtask) is queued as microtask.
  • The "End" is logged (synchronous), then the event loop processes the microtask, logging "Microtask".

What are Macrotasks

Macrotasks are tasks that are executed after the microtask queue has been emptied and before the next event loop cycle starts. These tasks represent operations like I/O or rendering and are usually scheduled after a certain event or after a delay.

Examples of macrotasks:

  • setTimeout()
  • setInterval()
  • setImmediate() (in Node.js)
  • I/O callbacks (file reading/writing)
  • UI rendering tasks (in browsers)

Code Example:

console.log("Start");

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

console.log("End");

Output:

Start
End
Macrotask

Explanation:

  • The code first logs "Start", which is synchronous.
  • The setTimeout() (macrotask) is queued.
  • The "End" is logged (synchronous), then the event loop processes the macrotask, logging "Macrotask".

Microtasks vs Macrotasks

Aspect Microtasks Macrotasks
Execution Timing Executed immediately after the current script, before rendering Executed in the next event loop iteration
Queue Priority Higher priority, processed before macrotasks Lower priority, processed after all microtasks are complete
Examples Promises, queueMicrotask(), MutationObserver setTimeout(), setInterval(), I/O operations, UI rendering
Use Case For tasks that need to be executed as soon as possible without yielding to the event loop For tasks that can be deferred or don't require immediate execution

事件循環

事件循環是 JavaScript 中的一個基本概念,儘管 JavaScript 是單線程的,但它仍然可以實現非阻塞非同步操作。它負責處理非同步回調並確保 JavaScript 繼續平穩運行,而不會被耗時的操作阻塞。

什麼是事件循環

事件循環是一種允許 JavaScript 高效處理非同步操作的機制。它不斷檢查呼叫堆疊和任務佇列(或微任務佇列)以確定接下來應該執行哪個函數。

為了更好地理解事件循環,了解 JavaScript 內部的工作原理非常重要。值得注意的是,JavaScript 是一種單執行緒語言,這意味著它一次只能做一件事。只有一個呼叫堆疊,它儲存要執行的函數。這使得同步程式碼變得簡單,但它給從伺服器獲取資料或設定超時等需要時間才能完成的任務帶來了問題。如果沒有事件循環,JavaScript 將陷入等待這些任務的狀態,並且不會發生其他任何事情。

事件循環如何運作

1. 呼叫棧:

呼叫堆疊是保存目前正在執行的函數的地方。 JavaScript 在處理程式碼時會在呼叫堆疊中新增和刪除函數。

2、非同步任務啟動:

當遇到像 setTimeout、fetch 或 Promise 這樣的非同步任務時,JavaScript 會將該任務委託給瀏覽器的 Web API(例如 Timer API、Network API 等),後者會在背景處理該任務。

3. 任務移至任務佇列:

一旦非同步任務完成(例如,計時器完成,或從伺服器接收到資料),回調(處理結果的函數)就會被移至任務佇列(或在Promise 的情況下為微任務佇列) .

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

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

5. 重複:

這個過程會重複。事件循環確保目前同步任務完成後處理所有非同步任務。

範例

現在我們對事件循環的工作原理有了更好、更清晰的了解,讓我們看一些例子來鞏固我們的理解。

Example 1: Timer with Promises and Event Loop

function exampleOne() {
  console.log("Start");

  setTimeout(() => {
    console.log("Timeout done");
  }, 1000);

  Promise.resolve().then(() => {
    console.log("Resolved");
  });

  console.log("End");
}

exampleOne();

Output:

Start
End
Resolved
Timeout done

Explanation:

  • Step 1: "Start" is printed (synchronous).
  • Step 2: setTimeout schedules the "Timeout done" message after 1 second (macrotask queue).
  • Step 3: A promise is resolved, and the "Resolved" message is pushed to the microtask queue.
  • Step 4: "End" is printed (synchronous).
  • Step 5: The call stack is now empty, so the microtask queue runs first, printing "Resolved".
  • Step 6: After 1 second, the macrotask queue runs, printing "Timeout done".

Example 2: Nested Promises and Timers

function exampleTwo() {
  console.log("Start");

  setTimeout(() => {
    console.log("Timer 1");
  }, 0);

  Promise.resolve().then(() => {
    console.log("Promise 1 Resolved");

    setTimeout(() => {
      console.log("Timer 2");
    }, 0);

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

  console.log("End");
}

exampleTwo();

Output:

Start
End
Promise 1 Resolved
Promise 2 Resolved
Timer 1
Timer 2

Explanation:

  • Step 1: "Start" is printed (synchronous).
  • Step 2: The first setTimeout schedules "Timer 1" to run (macrotask queue).
  • Step 3: The promise resolves, and its callback is pushed to the microtask queue.
  • Step 4: "End" is printed (synchronous).
  • Step 5: The microtask queue runs first:
    • "Promise 1 Resolved" is printed.
    • "Timer 2" is scheduled (macrotask queue).
    • Another promise is resolved, and "Promise 2 Resolved" is printed.
  • Step 6: The macrotask queue is processed next:
    • "Timer 1" is printed.
    • "Timer 2" is printed last.

Example 3: Mixed Synchronous and Asynchronous Operations

function exampleThree() {
  console.log("Step 1: Synchronous");

  setTimeout(() => {
    console.log("Step 2: Timeout 1");
  }, 0);

  Promise.resolve().then(() => {
    console.log("Step 3: Promise 1 Resolved");

    Promise.resolve().then(() => {
      console.log("Step 4: Promise 2 Resolved");
    });

    setTimeout(() => {
      console.log("Step 5: Timeout 2");
    }, 0);
  });

  setTimeout(() => {
    console.log(
      "Step 6: Immediate (using setTimeout with 0 delay as fallback)"
    );
  }, 0);

  console.log("Step 7: Synchronous End");
}

exampleThree();

Output:

Step 1: Synchronous
Step 7: Synchronous End
Step 3: Promise 1 Resolved
Step 4: Promise 2 Resolved
Step 2: Timeout 1
Step 6: Immediate (using setTimeout with 0 delay as fallback)
Step 5: Timeout 2

Explanation:

  • Step 1: "Step 1: Synchronous" is printed (synchronous).
  • Step 2: The first setTimeout schedules "Step 2: Timeout 1" (macrotask queue).
  • Step 3: A promise resolves, scheduling "Step 3: Promise 1 Resolved" (microtask queue).
  • Step 4: Another synchronous log, "Step 7: Synchronous End", is printed.
  • Step 5: Microtask queue is processed:
    • "Step 3: Promise 1 Resolved" is printed.
    • "Step 4: Promise 2 Resolved" is printed (nested microtask).
  • Step 6: The macrotask queue is processed:
    • "Step 2: Timeout 1" is printed.
    • "Step 6: Immediate (using setTimeout with 0 delay as fallback)" is printed.
    • "Step 5: Timeout 2" is printed last.

Conclusion

In JavaScript, mastering synchronous and asynchronous operations, as well as understanding the event loop and how it handles tasks, is crucial for writing efficient and performant applications.

  • Synchronous functions run in sequence, blocking subsequent code until completion, while asynchronous functions (like setTimeout and promises) allow for non-blocking behavior, enabling efficient multitasking.
  • Microtasks (such as promises) have higher priority than macrotasks (such as setTimeout), meaning that the event loop processes microtasks immediately after the current execution, before moving to the macrotask queue.
  • The event loop is the core mechanism that allows JavaScript to handle asynchronous code by managing the execution order of tasks and ensuring that the call stack is clear before processing the next queue (microtask or macrotask).

The examples provided progressively illustrated the interaction between synchronous code, promises, timers, and the event loop. Understanding these concepts is key to mastering asynchronous programming in JavaScript, ensuring your code runs efficiently and avoids common pitfalls such as race conditions or unexpected execution orders.


Stay Updated and Connected

To ensure you don't miss any part of this series and to connect with me for more in-depth discussions on Software Development (Web, Server, Mobile or Scraping / Automation), push notifications, and other exciting tech topics, follow me on:

  • GitHub
  • Linkedin
  • X (Twitter)

Stay tuned and happy coding ?‍??

以上是了解 JavaScript 中的非同步程式設計:事件循環初學者指南的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn