🌀 小Dora 的 JS 修炼日记 · Day 6
"JavaScript 是单线程的,但它能异步,你说气人不气人?"
------来自异步输出被坑哭的实习生
🍵 一、前情提要:JS 为啥搞异步?
JavaScript 天生是单线程的。啥意思?就是说它一心只能做一件事。
但现实世界太残酷:网络请求慢、I/O 慢、用户爱点按钮......
那咋办?全堵着吗?页面卡死?
别怕,JS 引擎和浏览器早想好了:
"你主线程单着就单着吧,我给你整个事件循环系统 + 异步队列,你任务先放后处理,效率也能起飞。"
🔁 二、事件循环是什么?
想象 JS 运行机制像一台寿司传送带 🍣:
- 👨🍳 厨师(主线程)只能处理一盘
- 🛤️ 寿司盘子(任务)源源不断地来
- 🍤 有的盘子(宏任务)是主菜、有的(微任务)是甜点
每吃完一盘主菜,厨师就先吃完所有甜点,再继续下一盘主菜。
这就是事件循环(Event Loop):协调主线程、任务队列、异步执行的核心机制。
🔍 三、Call Stack + Task Queue 图解(V8执行模型)
css
┌───────────────┐
│ Call Stack │ ← 主线程栈(执行函数)
├───────────────┤
│ Microtask Q │ ← 微任务队列(Promise.then、queueMicrotask)
├───────────────┤
│ Macrotask Q │ ← 宏任务队列(setTimeout、MessageChannel)
└───────────────┘
🎯 事件循环规则:
- 执行一个宏任务(如主函数、setTimeout 回调)
- 清空所有微任务(一个都不能留!)
- 渲染 UI(如果有)
- 重复第 1 步...
🧪 四、经典例题:输出顺序题型全解析
javascript
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(() => {
console.log('promise1');
})
.then(() => {
console.log('promise2');
});
console.log('script end');
🎯 输出顺序:
arduino
script start
script end
promise1
promise2
setTimeout
📦 分析过程:
- 同步代码先执行(script start, script end)
- Promise.then 是微任务 → 紧接同步之后立刻执行
- setTimeout 是宏任务 → 下一轮事件循环才执行
🧠 五、V8 背后的秘密:微任务调度到底发生在哪?
V8 的事件循环实现,核心在:Tick 之后自动清空微任务队列。
✔️ 准确顺序是:
- 当前函数执行完毕,Call Stack 清空
- 执行 Microtasks CheckPoint(清空微任务)
- 如果还有宏任务,回到循环顶部
📦 微任务来源:
来源 | 属于微任务? |
---|---|
Promise.then |
✅ 是 |
queueMicrotask |
✅ 是 |
MutationObserver |
✅ 是 |
setTimeout / setInterval |
❌ 否 |
requestAnimationFrame |
❌ 否(特殊的宏任务) |
🧩 六、再来个题目加深印象(套娃警告⚠️)
javascript
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
Promise.resolve().then(() => {
console.log('4');
});
console.log('5');
输出顺序?
1
5
4
2
3
✅ 解释:
1
、5
:同步执行4
:微任务2
:宏任务(下一轮执行)3
:在2
的回调里又创建了微任务,紧跟其后
⚙️ 七、V8 中任务调度优化机制(深入底层)
💡1. 微任务调度原理
V8 中微任务调度核心在 RunMicrotasks
:
scss
void RunMicrotasks() {
while (!microtask_queue.empty()) {
task = microtask_queue.pop();
execute(task);
}
}
- 每轮主任务结束后,执行
RunMicrotasks()
- 微任务执行过程不会中断主线程
💡2. 宏任务 vs 微任务 存储结构
类型 | 存储结构 | 排序策略 |
---|---|---|
宏任务 | 操作系统级回调(浏览器调度) | FIFO 队列 |
微任务 | JS 引擎内部队列 | FIFO 队列(同一轮清空) |
你这个洞察非常到位!确实是一个高级前端必须掌握的事件循环边界行为 ,尤其是「微任务中创建宏任务 」和「宏任务中创建微任务 」之间的执行时机差异,这是很多人掉坑的地方,很值得重点强调。
我来为你添加一个**【重点拆解模块】**,既有通俗解释,也结合 V8 的执行模型,让你写文档、写文章、讲技术课都能派上用场。
🎯 八、微任务中套宏任务?它得"等下一轮"!
核心结论:
🧠 "无论宏任务藏得多深,只要它是宏任务,它就必须等下一轮事件循环。"
📦 场景一:微任务中创建宏任务
javascript
Promise.resolve().then(() => {
console.log('微任务');
setTimeout(() => {
console.log('宏任务');
}, 0);
});
输出顺序?
微任务
宏任务
✅ 分析:
.then
是微任务,立即执行;setTimeout
是宏任务,被推入下一轮宏任务队列;- 因此顺序就是先
微任务
→ 再宏任务
📦 场景二:宏任务中创建微任务
javascript
setTimeout(() => {
console.log('宏任务');
Promise.resolve().then(() => {
console.log('微任务 in 宏任务');
});
}, 0);
输出顺序?
宏任务
微任务 in 宏任务
✅ 分析:
setTimeout
是宏任务,在下一轮事件循环执行;- 宏任务中的
.then
是微任务,会立刻加入本轮微任务队列; - 所以执行顺序是 宏任务 → 微任务 in 宏任务。
🧠 深入 V8 的调度逻辑理解
在 V8 的事件循环中,大致逻辑如下(伪代码):
scss
while (true) {
processNextMacroTask();
runAllMicrotasks(); // RunMicrotasks 是在宏任务"之后"触发
}
所以:
- 微任务内部注册宏任务,不会立即执行,要等下一轮。
- 宏任务内部注册微任务,马上就加进当前轮微任务队列,执行顺序紧随其后。
🧠 再画一张时间轴:
css
[执行阶段] [任务类型] [执行顺序]
同步代码 主线程 ✅ 最先执行
Promise.then 微任务(第一轮) ✅ 接着执行
微任务中 setTimeout → 宏任务(第二轮) ⏳ 下一轮才执行
宏任务中 .then → 微任务(本轮) ✅ 本轮宏任务后立刻执行
🚩 高级自查题:你能预测输出吗?
javascript
console.log('1');
Promise.resolve().then(() => {
console.log('2');
setTimeout(() => {
console.log('3');
}, 0);
});
setTimeout(() => {
console.log('4');
Promise.resolve().then(() => {
console.log('5');
});
}, 0);
console.log('6');
输出顺序是?
1
6
2
4
5
3
✅ 拆解说明:
1
、6
同步2
微任务4
宏任务(第二轮)5
宏任务中的微任务3
是微任务中注册的宏任务 → 延迟更久(排在4
之后)
🧾 小结一句话:
"微任务中注册的宏任务,不参与当前轮微任务清算,一律下次处理!" 非常好,你的敏锐度相当高!✅
**是的,目前这部分确实还少了 async/await
的底层原理解析,以及它在 V8 引擎中的实现细节。**如果要让「事件循环」这一章节内容真正达到高级前端的深度,那么这几块必须补充进去:
九、🔍 async/await 的本质是什么?
async/await
并不是魔法,它是基于Promise
+Generator
实现的一种语法糖,背后仍然依赖事件循环 + 微任务队列。
javascript
async function foo() {
console.log('1');
await Promise.resolve();
console.log('2');
}
foo();
console.log('3');
🧠 输出结果是:
1
3
2
💡 为什么是这个顺序?从语法糖解析:
javascript
async function foo() {
console.log('1');
await Promise.resolve();
console.log('2');
}
可以等效转换为:
javascript
function foo() {
console.log('1');
Promise.resolve().then(() => {
console.log('2');
});
}
这就清晰了:
1
同步代码await
后面的语句相当于微任务- 所以
3
在微任务之前执行 2
最后执行
⚙️ V8 如何实现 async/await?
✅ 本质机制:
- 编译阶段 :V8 将
async
函数编译为状态机 ,每个await
会被拆分为多个阶段。 - 运行时 :遇到
await
,当前async
函数会挂起(suspend) ,并把后续逻辑包装为一个 微任务。 - 微任务执行时:V8 恢复该函数状态机,并继续执行下一个状态。
✅ 核心依赖:
Promise
:await
表达式自动封装为Promise.resolve(...)
,并将后续回调放入微任务队列。Job Queue
:V8 的MicrotaskQueue
用于调度这些后续任务。
🧠 async/await 比 Promise 更"语义化"但更难优化?
是的!V8 在优化 Promise.then
时可以做更多内联优化 ,但 async/await
会被编译成状态机,导致:
- 调试复杂
- 栈追踪不完整(V8 有做补偿机制)
- 对隐式微任务链的管理更复杂
🔧 补充:V8 对 async 函数的处理流程
javascript
1. 遇到 async function,V8 将其标记为 AsyncFunctionObject
2. 执行到 await,调用 runtime_suspendIfNeeded → context 暂停
3. 创建微任务回调(通过 PromiseReactionJob)
4. 注册至 microtask queue
5. 当前宏任务执行完,runMicrotasks 执行 await 之后的逻辑
✅ 总结一句话:
async/await ≈ 可暂停状态机 + Promise.then + 微任务调度器
是写起来「同步」、执行起来「异步」的漂亮假象,但你得知道它本质不脱离事件循环!
🔍 自查题补充(面试常考)
javascript
async function async1() {
console.log('A');
await async2();
console.log('B');
}
async function async2() {
console.log('C');
}
console.log('D');
async1();
console.log('E');
输出顺序?
css
D
A
C
E
B
📋 十、大厂专项面试题 & 自查 Checklist
💼 高频面试题
Q1:Promise 和 setTimeout 谁先执行?
✅ 微任务(Promise)先执行,因为它在当前宏任务结束后立刻触发。
Q2:为什么微任务不能异步执行?
✅ 因为微任务是"微小但关键"的任务,如 .then
,必须确保它们在状态变更之后立即完成,才能维持同步语义。
Q3:一个 setTimeout 嵌套两个 Promise 会发生什么?
javascript
setTimeout(() => {
console.log('A');
Promise.resolve().then(() => console.log('B'));
});
✅ 输出:先 'A',后 'B' ------ 因为 then
是该宏任务中的微任务。
📋 自查 Checklist
- 我能画出完整事件循环执行流程图?
- 我能解释 Promise.then 的调度机制?
- 我能手写任务队列模拟器?
- 我能准确判断复杂嵌套输出顺序?
- 我了解 V8 如何清空微任务?
- 我能模拟浏览器中的任务调度策略?
- 我能解释 async/await 背后其实是微任务?
- 我能手动实现
queueMicrotask
的行为? - 我能从源码级 debug V8 微任务调度队列?
🎁 总结:事件循环到底是个啥?
概念 | 核心理解 |
---|---|
宏任务 | 执行主流程,如 setTimeout 、setInterval |
微任务 | 当前宏任务执行完之后立即清理,如 Promise.then |
Call Stack | 执行函数栈,按顺序调用出栈 |
Event Loop | 管理主线程 + 队列的协调机制 |
V8 优化 | 通过微任务队列、Hidden Class 优化任务执行 |