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

2025-06-08

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

你有没有想过,为什么有些 JavaScript 代码运行起来似乎毫无秩序?理解这一点的关键在于事件循环

JavaScript 的事件循环可能比较难理解,尤其是在处理不同类型的异步操作时。在本文中,我们将详细分析 JavaScript 如何处理同步异步代码、微任务宏任务,以及为什么某些事件会按特定顺序发生。

目录

  1. 同步和异步代码
  2. 微任务和宏任务
  3. 事件循环
  4. 示例
  5. 结论

同步和异步代码

JavaScript 主要以两种方式处理操作:同步异步。理解它们之间的区别是掌握 JavaScript 如何处理任务以及如何编写高效、非阻塞代码的关键。

什么是同步代码?

同步代码是 JavaScript 的默认设置,这意味着每行代码都会按顺序依次运行。例如:

console.log("First");
console.log("Second");
Enter fullscreen mode Exit fullscreen mode

这将输出:

First
Second
Enter fullscreen mode Exit fullscreen mode

什么是异步代码?

另一方面,异步代码允许某些任务在后台运行并稍后完成,而不会阻塞其余代码。setTimeout ()Promise之类的函数就是异步代码的示例。

下面是使用异步代码的简单示例setTimeout()

console.log("First");

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

console.log("Third");
Enter fullscreen mode Exit fullscreen mode

这将输出:

First
Third
Second
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode
  1. 承诺:承诺代表异步函数最终将返回的未来值(或错误)。

代码示例:

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");
Enter fullscreen mode Exit fullscreen mode
  1. Async/Await: Async/await 是在承诺之上构建的语法糖,允许我们编写看起来同步的异步代码。

代码示例:

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");
Enter fullscreen mode Exit fullscreen mode

同步代码与异步代码

为了更好地理解 JavaScript 的每种执行方法以及它们之间的区别,这里详细介绍了 JavaScript 函数在多个方面的差异。

方面 同步代码 异步代码
执行顺序 按顺序逐行执行 允许任务在后台运行,同时其他代码继续执行
表现 如果涉及长时间运行的任务,可能会导致性能问题 提高 I/O 密集型操作的性能;防止浏览器环境中的 UI 冻结
代码复杂性 通常更简单、更易读 可能更复杂,尤其是嵌套回调(回调地狱)
内存使用情况 如果等待长时间操作,可能会使用更多内存 对于长时间运行的任务来说,通常更节省内存
可扩展性 对于具有许多并发操作的应用程序来说,可扩展性较差 更具可扩展性,特别是对于处理多个同时操作的应用程序

这种比较突出了同步和异步代码之间的主要区别,帮助开发人员根据其特定用例和性能要求选择适当的方法。


微任务和宏任务

在 JavaScript 中,微任务和宏任务是两种类型的任务,它们在事件循环的不同部分排队并执行,这决定了 JavaScript 如何处理异步操作。

微任务和宏任务都在事件循环中排队执行,但它们的优先级和执行上下文不同。微任务会持续处理,直到微任务队列为空,然后才会转到宏任务队列中的下一个任务。而宏任务则会在微任务队列清空之后、下一个事件循环开始之前执行。

什么是微任务

微任务是指需要在当前操作完成后、下一个事件循环开始前执行的任务。微任务优先于宏任务,会持续处理,直到微任务队列为空,才会转至宏任务队列中的下一个任务。

微任务示例:

  • 承诺(使用.then().catch()处理程序时)
  • MutationObserver 回调(用于观察 DOM 的变化)
  • process.nextTick()Node.js 中的一些

代码示例

console.log("Start");

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

console.log("End");
Enter fullscreen mode Exit fullscreen mode

输出:

Start
End
Microtask
Enter fullscreen mode Exit fullscreen mode

解释:

  • 代码首先记录“Start”,这是同步的。
  • 承诺处理程序(微任务)作为微任务排队。
  • 记录“结束”(同步),然后事件循环处理微任务,记录“微任务”。

什么是宏任务

宏任务是在微任务队列清空之后、下一个事件循环开始之前执行的任务。这些任务代表诸如 I/O 或渲染之类的操作,通常在某个事件之后或延迟后进行调度。

宏任务示例:

  • 设置超时()
  • 设置间隔()
  • setImmediate() (在 Node.js 中)
  • I/O回调(文件读/写)
  • UI 渲染任务(在浏览器中)

代码示例:

console.log("Start");

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

console.log("End");
Enter fullscreen mode Exit fullscreen mode

输出:

Start
End
Macrotask
Enter fullscreen mode Exit fullscreen mode

解释:

  • 代码首先记录“Start”,这是同步的。
  • setTimeout()(宏任务)已排队。
  • 记录“结束”(同步),然后事件循环处理宏任务,记录“宏任务”。

微任务与宏任务

方面 微任务 宏任务
执行时间 在当前脚本之后、渲染之前立即执行 在下一个事件循环迭代中执行
队列优先级 优先级较高,在宏任务之前处理 优先级较低,所有微任务完成后处理
示例 承诺、queueMicrotask()、MutationObserver setTimeout()、setInterval()、I/O操作、UI渲染
用例 对于需要尽快执行且不让步于事件循环的任务 对于可以推迟或不需要立即执行的任务

事件循环

事件循环是 JavaScript 中的一个基本概念,它允许 JavaScript 在单线程的情况下执行非阻塞异步操作。它负责处理异步回调,并确保 JavaScript 持续平稳运行,而不会被耗时的操作阻塞。

什么是事件循环

事件循环是一种允许 JavaScript 高效处理异步操作的机制。它不断检查调用堆栈和任务队列(或微任务队列),以确定接下来应该执行哪个函数。

为了更好地理解事件循环,了解 JavaScript 的内部工作原理至关重要。需要注意的是,JavaScript 是一种单线程语言,这意味着它一次只能执行一项操作。它只有一个调用堆栈,用于存储要执行的函数。这使得同步代码变得简单易懂,但对于诸如从服务器获取数据或设置超时等需要时间才能完成的任务,它会带来问题。如果没有事件循环,JavaScript 就会陷入等待这些任务的困境,其他任何事情都无法发生。

事件循环如何工作

1.调用堆栈:

调用栈是当前正在执行的函数的保存位置。JavaScript 在处理代码时会在调用栈中添加和删除函数。

2.异步任务启动:

当遇到像 setTimeout、fetch 或 Promise 这样的异步任务时,JavaScript 会将该任务委托给浏览器的 Web API(如 Timer API、Network API 等),这些 API 在后台处理该任务。

3. 任务移至任务队列:

一旦异步任务完成(例如,计时器完成或从服务器接收到数据),回调​​(处理结果的函数)就会被移动到任务队列(或者在承诺的情况下是微任务队列)。

4.调用堆栈完成当前执行:

JavaScript 继续执行同步代码。一旦调用栈为空,事件循环就会从任务队列(或微任务队列)中取出第一个任务,并将其放入调用栈执行。

5.重复:

这个过程不断重复。事件循环确保当前同步任务完成后,所有异步任务都得到处理。

示例

现在我们对事件循环的工作原理有了更好、更清晰的理解,让我们看一些例子来巩固我们的理解。

示例 1:带有 Promises 和事件循环的计时器

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

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

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

  console.log("End");
}

exampleOne();
Enter fullscreen mode Exit fullscreen mode

输出:

Start
End
Resolved
Timeout done
Enter fullscreen mode Exit fullscreen mode

解释:

  • 步骤1:打印“开始”(同步)。
  • 第二步: setTimeout调度1秒后(宏任务队列)发送“超时完成”消息。
  • 步骤3: promise被解决,并且“Resolved”消息被推送到微任务队列。
  • 步骤4:打印“结束”(同步)。
  • 步骤 5:调用堆栈现在为空,因此微任务队列首先运行,打印“Resolved”。
  • 第六步: 1秒后,宏任务队列运行,打印“Timeout done”。

示例 2:嵌套的 Promise 和计时器

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();
Enter fullscreen mode Exit fullscreen mode

输出:

Start
End
Promise 1 Resolved
Promise 2 Resolved
Timer 1
Timer 2
Enter fullscreen mode Exit fullscreen mode

解释:

  • 步骤1:打印“开始”(同步)。
  • 第二步:第一个setTimeout调度“定时器1”运行(宏任务队列)。
  • 步骤 3:承诺得到解决,其回调被推送到微任务队列。
  • 步骤4:打印“结束”(同步)。
  • 第五步:微任务队列首先运行:
    • 打印“Promise 1 Resolved”。
    • “定时器2”已被调度(宏任务队列)。
    • 另一个承诺已得到解决,并打印“Promise 2 Resolved”。
  • 第六步:接下来处理宏任务队列:
    • 打印“定时器 1”。
    • 最后打印“定时器 2”。

示例 3:混合同步和异步操作

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();
Enter fullscreen mode Exit fullscreen mode

输出:

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
Enter fullscreen mode Exit fullscreen mode

解释:

  • 步骤1:打印“步骤1:同步”(同步)。
  • 步骤2:第一个setTimeout调度“步骤2:超时1”(宏任务队列)。
  • 步骤 3:承诺解决,安排“步骤 3:承诺 1 解决”(微任务队列)。
  • 步骤4:再打印一条同步日志“步骤7:同步结束”。
  • 步骤5:处理微任务队列:
    • 打印“步骤 3:承诺 1 已解决”。
    • 打印“步骤 4:Promise 2 Resolved”(嵌套微任务)。
  • 第六步:宏任务队列处理:
    • 打印“步骤 2:超时 1”。
    • 打印“步骤 6:立即(使用 setTimeout 并以 0 延迟作为后备)”。
    • 最后打印“步骤 5:超时 2”。

结论

在 JavaScript 中,掌握同步和异步操作以及理解事件循环及其处理任务的方式对于编写高效且性能良好的应用程序至关重要。

  • 同步函数按顺序运行,阻止后续代码直到完成,而异步函数(如 setTimeout 和 promises)允许非阻塞行为,从而实现高效的多任务处理。
  • 微任务(例如 Promise)比宏任务(例如 setTimeout)具有更高的优先级,这意味着事件循环在当前执行之后立即处理微任务,然后再移至宏任务队列。
  • 事件循环是 JavaScript 处理异步代码的核心机制,它通过管理任务的执行顺序并确保在处理下一个队列(微任务或宏任务)之前调用堆栈清晰来实现。

提供的示例逐步演示了同步代码、Promise、计时器和事件循环之间的交互。理解这些概念是掌握 JavaScript 异步编程的关键,它能确保代码高效运行,并避免诸如竞争条件或意外的执行顺序等常见陷阱。


保持更新和联系

为了确保您不会错过本系列的任何部分,并与我联系,就软件开发(Web、服务器、移动或抓取/自动化)、推送通知和其他令人兴奋的技术主题进行更深入的讨论,请关注我:

敬请期待,祝您编码愉快👨‍💻🚀

鏂囩珷鏉簮锛�https://dev.to/emmanuelayinde/understanding-asynchronous-programming-in-javascript-synchronous-asynchronous-microtasks-macrotasks-and-the-event-loop-h5e
PREV
Docker 基础知识
NEXT
您的技术招聘邮件失败的原因案例研究 1:冗长且含糊不清案例研究 2:完全错误的角色案例研究 3:您是完美人选,但您认识其他人吗?结论