深入理解 JavaScript 事件循环:同步与异步执行机制

小石头
小石头
发布时间:2026-10-16| 5人浏览过
原创

JavaScript 作为单线程语言,其核心难点在于如何处理同步与异步任务的执行顺序。本文将深入剖析事件循环(Event Loop)机制,解释执行栈、任务队列以及宏任务与微任务的协作关系,并通过实例演示确保你能准确预测代码输出结果。

为什么 JavaScript 是单线程的?

JavaScript 的设计初衷主要是为了与浏览器页面进行交互(如处理用户点击、DOM 操作)。如果采用多线程模型,可能会引发严重的冲突。例如,一个线程试图增加 DOM 节点,而另一个线程同时删除该节点,浏览器将难以判断最终状态,导致 UI 渲染混乱且复杂。

此外,如果没有异步机制,所有操作均为同步时,遇到网络请求或耗时计算,整个页面将被阻塞,导致用户无法交互,体验极差。因此,引入异步机制和事件循环成为了解决这一问题的关键。

执行栈与任务队列

执行栈(Call Stack)

执行栈遵循“先进后出”(LIFO)的原则,就像叠放盘子一样。在 JavaScript 中,所有同步任务都会被压入执行栈中依次执行。

以递归函数为例:

function fn(count) {
    if (count <= 0) return;
    fn(count - 1);
    console.log(count);
}
fn(3);

这段代码的执行过程如下:每次调用 fn 都会向栈顶压入一个新的执行上下文。当 count 递减至 0 触发返回条件时,栈顶的上下文才会依次弹出并执行 console.log。

需要注意的是,JS 引擎的全局代码本身就是一个执行上下文,位于栈底。所有任务在主线程上运行时,都必须经过这个执行栈。

任务队列(Task Queue)

与执行栈不同,任务队列遵循“先进先出”(FIFO)原则,类似于排队买票。它专门用于存放异步任务完成后的回调函数。只有当执行栈为空时,主线程才会从任务队列中取出回调函数放入执行栈中运行。

同步任务与异步任务

同步任务(Synchronous)

同步任务是指在主线程上排队执行的任务,前一个任务结束后,才能执行下一个任务。这确保了代码执行的顺序性。

console.log(1);
console.log(2);
console.log(3);

输出结果为 1, 2, 3。这是因为所有语句都是同步代码,直接按顺序进入执行栈并立即执行。

异步任务(Asynchronous)

异步任务不会直接进入主线程,而是交由宿主环境(如浏览器或 Node.js)处理。处理完成后,将对应的回调函数放入任务队列等待执行。

console.log(1);
setTimeout(() => {
    console.log(2);
}, 1000);
console.log(3);

尽管 setTimeout 写在 console.log(3) 之前,但其输出结果依然是 1, 3, 2。这是因为:
1. console.log(1) 同步执行。
2. setTimeout 是异步任务,被挂起并放入任务队列(设定延迟1秒)。
3. console.log(3) 同步执行。
4. 执行栈清空后,定时器到达时间,回调进入执行栈,输出 2。

JavaScript 的事件循环(Event Loop)机制

理解 JS 执行机制的关键在于掌握以下四个步骤的循环过程:

  • 同步任务执行:JavaScript 主线程按顺序执行所有同步代码。
  • 异步任务委托:异步任务交给宿主环境处理(如定时器、网络请求)。
  • 回调加入队列:异步任务完成后,其回调函数被添加到任务队列中。任务队列分为宏任务(MacroTask)和微任务(MicroTask)。
  • 空栈读取:当执行栈中的所有同步任务执行完毕(栈为空)时,主线程会优先检查微任务队列,执行所有微任务;然后再取出一个宏任务放入栈中执行。如此反复,形成循环。

宏任务与微任务的优先级

在任务队列中,**微任务的优先级高于宏任务**。常见的微任务包括 Promise.then、MutationObserver 等;常见的宏任务包括 setTimeout、setInterval、I/O 操作等。

综合案例分析

下面我们通过一段包含 Promise、定时器(宏任务)和同步代码的综合案例,逐步拆解执行流程:

const promise = new Promise((resolve, reject) => {
    console.log(1);
    setTimeout(() => {
        console.log("timerStart");
        resolve("success");
        console.log("timerEnd");
    }, 0);
    console.log(2);
});

promise.then((res) => {
    console.log(res);
});

console.log(4);

第一步:识别任务类型

  • new Promise 内部是同步代码,立即执行。
  • promise.then 是异步微任务,回调暂存。
  • setTimeout 是异步宏任务,回调暂存。
  • 最后的 console.log(4) 是同步代码。

第二步:执行主线程同步代码

  1. 执行 new Promise:内部同步代码执行,输出 1。
  2. 遇到 setTimeout:注册定时器。由于延迟为 0,一旦时机允许即加入宏任务队列。
  3. 继续执行:输出 2。
  4. 跳出 Promise 构造函数:此时主线程遇到 promise.then,因为 Promise 状态尚未改变,其回调未放入队列,而是等待后续触发。
  5. 执行最后语句:输出 4。

第三步:清空执行栈,开始读取任务队列

此时同步代码全部执行完毕,执行栈为空。按照规则,优先处理微任务。

  • 微任务检查:当前微任务队列为空吗?是的。因为前面 Promise 的状态还没有变成 resolved,所以 .then() 的回调还没产生,不在队列里。
  • 宏任务检查:从宏任务队列取出唯一的 setTimeout 回调放入栈中执行。
  • 执行宏任务回调内容:
    1. 输出 timerStart。
    2. 调用 resolve("success"):改变 Promise 状态为 resolved。
    3. 此时,之前注册的 promise.then 回调被加入到微任务队列。
    4. 输出 timerEnd。

第四步:再次检查任务队列

宏任务执行完毕后,执行栈再次变空。回到步骤三的逻辑,再次优先检查微任务队列。

  • 微任务执行:现在队列里有刚才加入的 promise.then 回调了。
  • 执行回调:输出 Promise 的值,即 success。

总结

JavaScript 的单线程特性要求我们必须借助事件循环来处理耗时操作。核心原则可以概括为:同步任务先进执行栈执行,异步任务进任务队列等待;执行栈清空后,优先执行完所有微任务,再执行一个宏任务。掌握这一规律,便能轻松应对各类复杂的并发代码执行顺序问题。

以上就是深入理解 JavaScript 事件循环:同步与异步执行机制的详细内容,更多关于 JavaScript 事件循环、Promise 及并发编程的资料请关注本站其它相关文章!

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。