事件循环:JavaScript 的隐形排队大师

🌀 小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)
└───────────────┘

🎯 事件循环规则:

  1. 执行一个宏任务(如主函数、setTimeout 回调)
  2. 清空所有微任务(一个都不能留!)
  3. 渲染 UI(如果有)
  4. 重复第 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 之后自动清空微任务队列

✔️ 准确顺序是:

  1. 当前函数执行完毕,Call Stack 清空
  2. 执行 Microtasks CheckPoint(清空微任务)
  3. 如果还有宏任务,回到循环顶部

📦 微任务来源:

来源 属于微任务?
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

✅ 解释:

  • 15:同步执行
  • 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);
});

输出顺序?

复制代码
微任务
宏任务

✅ 分析:

  1. .then 是微任务,立即执行;
  2. setTimeout 是宏任务,被推入下一轮宏任务队列;
  3. 因此顺序就是先 微任务 → 再 宏任务

📦 场景二:宏任务中创建微任务

javascript 复制代码
setTimeout(() => {
  console.log('宏任务');

  Promise.resolve().then(() => {
    console.log('微任务 in 宏任务');
  });
}, 0);

输出顺序?

复制代码
宏任务
微任务 in 宏任务

✅ 分析:

  1. setTimeout 是宏任务,在下一轮事件循环执行;
  2. 宏任务中的 .then 是微任务,会立刻加入本轮微任务队列
  3. 所以执行顺序是 宏任务 → 微任务 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

✅ 拆解说明:

  • 16 同步
  • 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?

✅ 本质机制:
  1. 编译阶段 :V8 将 async 函数编译为状态机 ,每个 await 会被拆分为多个阶段。
  2. 运行时 :遇到 await,当前 async 函数会挂起(suspend) ,并把后续逻辑包装为一个 微任务
  3. 微任务执行时:V8 恢复该函数状态机,并继续执行下一个状态。
✅ 核心依赖:
  • Promiseawait 表达式自动封装为 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 微任务调度队列?

🎁 总结:事件循环到底是个啥?

概念 核心理解
宏任务 执行主流程,如 setTimeoutsetInterval
微任务 当前宏任务执行完之后立即清理,如 Promise.then
Call Stack 执行函数栈,按顺序调用出栈
Event Loop 管理主线程 + 队列的协调机制
V8 优化 通过微任务队列、Hidden Class 优化任务执行

相关推荐
AiMuo2 分钟前
FLJ性能战争战报:完全抛弃 Next.js 打包链路,战术背断性选择 esbuild 自建 Worker 脚本经验
前端·性能优化
Lefan2 分钟前
解决重复请求与取消未响应请求
前端
混水的鱼3 分钟前
React + antd 实现文件预览与下载组件(支持图片、PDF、Office)
前端·react.js
程序员嘉逸8 分钟前
🎨 CSS属性完全指南:从入门到精通的样式秘籍
前端
Jackson_Mseven21 分钟前
🧺 Monorepo 是什么?一锅端的大杂烩式开发幸福生活
前端·javascript·架构
我想说一句29 分钟前
JavaScript数组:轻松愉快地玩透它
前端·javascript
binggg31 分钟前
AI 编程不靠运气,Kiro Spec 工作流复刻全攻略
前端·claude·cursor
晓131336 分钟前
JavaScript进阶篇——第七章 原型与构造函数核心知识
开发语言·javascript·ecmascript
天天摸鱼的java工程师37 分钟前
如何防止重复提交订单?
java·后端·面试
ye空也晴朗40 分钟前
基于eggjs+mongodb实现后端服务
前端