用事件循环详细分析一个简单的 promise 案例
基础概念
更多关于事件循环的概念,可见 JS 事件循环
为了彻底理解,我们首先要明确 JavaScript 运行时环境的几个核心组成部分:
-
调用栈 (Call Stack):一个后进先出(LIFO)的数据结构。当一个函数被调用时,它会被推入栈中;当函数执行完毕返回时,它会从栈中被弹出。JavaScript 主线程的所有同步代码都在这里执行。
-
堆 (Heap):一块内存区域,用于存储对象、数组等复杂数据结构。我们代码中的
promise对象就存放在这里。 -
Web APIs / C++ APIs:由浏览器(或 Node.js)提供的 API,它们不是 JavaScript 引擎的一部分。例如
setTimeout,DOM 事件,AJAX (fetch)等异步操作。它们在后台执行,不会阻塞主线程。 -
任务队列 (Task Queue / Macrotask Queue):一个先进先出(FIFO)的队列。当 Web API 完成其任务后(比如
setTimeout的计时器到期),会将其对应的回调函数放入这个队列中。 -
微任务队列 (Microtask Queue):另一个先进先出(FIFO)的队列,但它的优先级高于任务队列。
Promise的.then(),.catch(),.finally()的回调函数,以及queueMicrotask()和MutationObserver的回调会进入这个队列。 -
事件循环 (Event Loop):一个持续运行的进程,它的核心职责是:
-
监控调用栈是否为空。
-
如果调用栈为空,它会去检查微任务队列。
-
如果微任务队列不为空,它会依次执行队列中所有的微任务,直到微任务队列清空。
-
当微任务队列清空后,它会去任务队列(宏任务队列)取出一个任务(如果有的话),并将其推入调用栈执行。
-
重复以上过程。
-
代码执行全过程详解
我们把整个过程分解成详细的步骤,并追踪每个核心部分的状态。
初始状态
-
调用栈: 空
-
Web APIs: 空
-
微任务队列: 空
-
任务队列: 空
第 1 步:全局代码执行(作为第一个宏任务)
-
整个
script标签中的代码开始作为第一个宏任务执行。全局执行上下文被推入调用栈。 -
const promise = new Promise(...)被执行。 -
关键点:
Promise的构造函数new Promise(executor)中的executor函数(resolve, reject) => { ... }是立即同步执行的。 -
executor函数被推入调用栈。 -
在
executor内部,if (true)条件成立。 -
setTimeout(...)被调用。setTimeout是一个 Web API,它不会在调用栈里等待。JavaScript 引擎将它交给浏览器的 Web API 环境处理。 -
Web API 环境接收到
setTimeout的指令,开始一个 2000毫秒 的计时器。同时,它会持有() => { resolve(...) }这个回调函数。 -
setTimeout函数本身执行完毕,从调用栈中弹出。 -
executor函数执行完毕,从调用栈中弹出。 -
promise对象已创建,其初始状态为pending。 -
代码继续向下执行,遇到
promise.then(...)。由于promise状态还是pending,.then里的回调函数(params) => { console.log([params]); }被注册到该promise内部的 “onFulfilled” 列表中,等待被触发。 -
代码继续向下执行,遇到
.catch(...)。同样,.catch里的回调函数被注册到该promise内部的 “onRejected” 列表中。 -
全局脚本的所有同步代码执行完毕。全局执行上下文从调用栈中弹出。
此时的状态 (在 2 秒计时器结束前):
-
调用栈: 空
-
Web APIs: 正在运行一个 2000ms 的计时器,关联着一个回调函数。
-
微任务队列: 空
-
任务队列: 空
-
Promise 对象: 状态为
pending。
第 2 步:计时器到期
-
大约 2000 毫秒后,Web API 环境中的计时器完成。
-
Web API 将它持有的回调函数
() => { resolve("我是传给 .then 的值...") }放入任务队列(宏任务队列)中排队。
此时的状态 (刚过 2000ms):
-
调用栈: 空
-
Web APIs: 计时器已完成。
-
微任务队列: 空
-
任务队列:
[() => { resolve(...) }](有一个待执行的宏任务)
第 3 步:事件循环处理宏任务
-
事件循环发现调用栈是空的。
-
它检查微任务队列,发现也是空的。
-
然后它检查任务队列,发现有一个任务
() => { resolve(...) }。 -
事件循环将这个任务从任务队列中取出,并将其推入调用栈执行。
此时的状态:
-
调用栈:
[() => { resolve(...) }] -
微任务队列: 空
-
任务队列: 空
第 4 步:Promise 状态变更并触发微任务
-
调用栈中的
() => { resolve(...) }函数开始执行。 -
resolve("我是传给 .then 的值...")被调用。 -
核心关键点:resolve() 函数的调用会做两件事:
a. 将 promise 对象的状态从 pending 变为 fulfilled,并保存结果值为 “我是传给 .then 的值…”。
b. 检查该 promise 上是否有注册的 “onFulfilled” 回调(我们在第1步通过 .then() 注册了)。它发现有一个。
c. 这个 “onFulfilled” 回调 (params) => { console.log([params]); } 被放入微任务队列中。
-
resolve()函数执行完毕。 -
() => { resolve(...) }这个宏任务回调函数执行完毕,从调用栈中弹出。
此时的状态:
-
调用栈: 空
-
微任务队列:
[(params) => { console.log([params]); }](有一个待执行的微任务) -
任务队列: 空
第 5 步:事件循环处理微任务
-
在一个宏任务(第4步中的
setTimeout回调)执行完毕后,并且在开始下一个宏任务之前,事件循环必须清空整个微任务队列。 -
事件循环检查到微任务队列不为空。
-
它从微任务队列中取出
.then的回调函数(params) => { console.log([params]); },并将其推入调用栈。
此时的状态:
-
调用栈:
[(params) => { console.log(...) }] -
微任务队列: 空
-
任务队列: 空
第 6 步:执行 .then 的回调
-
调用栈中的
.then回调函数开始执行。 -
参数
params接收到promise在resolve时传递的值,即"我是传给 .then 的值,会被作为 .then 的参数"。 -
console.log([params])被执行。 -
控制台输出:
["我是传给 .then 的值,会被作为 .then 的参数"]。 -
该回调函数执行完毕,从调用栈中弹出。
此时的状态 (最终):
-
调用栈: 空
-
微任务队列: 空
-
任务队列: 空
-
整个程序执行完毕,等待新的事件。
总结
这个过程的核心在于理解不同任务的调度时机:
-
new Promise的executor是同步执行的。 -
setTimeout的回调是一个宏任务 (Macrotask),它由 Web API 在计时结束后放入任务队列。 -
promise.then的回调是一个微任务 (Microtask),它在promise状态变为fulfilled时被放入微任务队列。 -
事件循环在一个 “tick” 中,会先执行完调用栈中的同步代码,然后清空所有微任务,最后才会去取一个宏任务来执行。这个优先级是整个异步流程的关键。
- 标题: 用事件循环详细分析一个简单的 promise 案例
- 作者: 三葉Leaves
- 创建于 : 2025-07-25 00:00:00
- 更新于 : 2026-03-16 12:05:06
- 链接: https://blog.oksanye.com/dcfb1de47793/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。