用事件循环详细分析一个简单的 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
- 更新于 : 2025-08-13 16:31:27
- 链接: https://blog.oksanye.com/dcfb1de47793/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。