首页 > web前端 > js教程 > 正文

了解 JavaScript 中的异步编程:事件循环初学者指南

PHPz
发布: 2024-09-11 06:32:03
原创
787 人浏览过

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中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责声明 Sitemap
PHP中文网:公益在线PHP培训,帮助PHP学习者快速成长!