首页/文章/八股文

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

2025-07-20
310011 分钟
...

 

🍵 一、前情提要:JS 为啥搞异步?

JavaScript 天生是单线程的。啥意思?就是说它一心只能做一件事

但现实世界太残酷:网络请求慢、I/O 慢、用户爱点按钮……
那咋办?全堵着吗?页面卡死?

别怕,JS 引擎和浏览器早想好了:

“你主线程单着就单着吧,我给你整个事件循环系统 + 异步队列,你任务先放后处理,效率也能起飞。”


🔁 二、事件循环是什么?

想象 JS 运行机制像一台寿司传送带 🍣:

  • 👨‍🍳 厨师(主线程)只能处理一盘
  • 🛤️ 寿司盘子(任务)源源不断地来
  • 🍤 有的盘子(宏任务)是主菜、有的(微任务)是甜点

每吃完一盘主菜,厨师就先吃完所有甜点,再继续下一盘主菜。

这就是事件循环(Event Loop):协调主线程、任务队列、异步执行的核心机制。


🔍 三、Call Stack + Task Queue 图解(V8执行模型)

┌───────────────┐
Call Stack  │   ← 主线程栈执行函数
├───────────────┤
Microtask Q   │   ← 微任务队列Promise.thenqueueMicrotask
├───────────────┤
Macrotask Q   │   ← 宏任务队列setTimeoutMessageChannel
└───────────────┘

🎯 事件循环规则:

  1. 执行一个宏任务(如主函数、setTimeout 回调)
  2. 清空所有微任务(一个都不能留!)
  3. 渲染 UI(如果有)
  4. 重复第 1 步…

🧪 四、经典例题:输出顺序题型全解析

console.log('script start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

Promise.resolve()
  .then(() => {
    console.log('promise1');
  })
  .then(() => {
    console.log('promise2');
  });

console.log('script end');

🎯 输出顺序:

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❌ 否(特殊的宏任务)

🧩 六、再来个题目加深印象(套娃警告⚠️)

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

void RunMicrotasks() {
  while (!microtask_queue.empty()) {
    task = microtask_queue.pop();
    execute(task);
  }
}
  • 每轮主任务结束后,执行 RunMicrotasks()
  • 微任务执行过程不会中断主线程

💡2. 宏任务 vs 微任务 存储结构

类型存储结构排序策略
宏任务操作系统级回调(浏览器调度)FIFO 队列
微任务JS 引擎内部队列FIFO 队列(同一轮清空)

你这个洞察非常到位!确实是一个高级前端必须掌握的事件循环边界行为,尤其是「微任务中创建宏任务」和「宏任务中创建微任务」之间的执行时机差异,这是很多人掉坑的地方,很值得重点强调

我来为你添加一个**【重点拆解模块】**,既有通俗解释,也结合 V8 的执行模型,让你写文档、写文章、讲技术课都能派上用场。


🎯 八、微任务中套宏任务?它得“等下一轮”!

核心结论:
🧠 “无论宏任务藏得多深,只要它是宏任务,它就必须等下一轮事件循环。”


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

Promise.resolve().then(() => {
  console.log('微任务');

  setTimeout(() => {
    console.log('宏任务');
  }, 0);
});

输出顺序?

微任务
宏任务

✅ 分析:

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

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

setTimeout(() => {
  console.log('宏任务');

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

输出顺序?

宏任务
微任务 in 宏任务

✅ 分析:

  1. setTimeout 是宏任务,在下一轮事件循环执行;
  2. 宏任务中的 .then 是微任务,会立刻加入本轮微任务队列
  3. 所以执行顺序是 宏任务 → 微任务 in 宏任务。

🧠 深入 V8 的调度逻辑理解

在 V8 的事件循环中,大致逻辑如下(伪代码):

while (true) {
  processNextMacroTask();

  runAllMicrotasks(); 
}

所以:

  • 微任务内部注册宏任务,不会立即执行,要等下一轮。
  • 宏任务内部注册微任务,马上就加进当前轮微任务队列,执行顺序紧随其后。

🧠 再画一张时间轴:

[执行阶段]        [任务类型]                  [执行顺序]

 同步代码           主线程最先执行

 Promise.then      微任务第一轮)           ✅ 接着执行

 微任务中 setTimeout宏任务第二轮)     ⏳ 下一轮才执行

 宏任务中 .then微任务本轮)           ✅ 本轮宏任务后立刻执行

🚩 高级自查题:你能预测输出吗?

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 实现的一种语法糖,背后仍然依赖事件循环 + 微任务队列。

async function foo() {
  console.log('1');
  await Promise.resolve();
  console.log('2');
}
foo();
console.log('3');

🧠 输出结果是:

1
3
2

💡 为什么是这个顺序?从语法糖解析:

async function foo() {
  console.log('1');
  await Promise.resolve(); 
  console.log('2');
}

可以等效转换为:

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 函数的处理流程

1. 遇到 async functionV8 将其标记为 AsyncFunctionObject
2. 执行到 await调用 runtime_suspendIfNeededcontext 暂停
3. 创建微任务回调通过 PromiseReactionJob
4. 注册至 microtask queue
5. 当前宏任务执行完runMicrotasks 执行 await 之后的逻辑

✅ 总结一句话:

async/await ≈ 可暂停状态机 + Promise.then + 微任务调度器
是写起来「同步」、执行起来「异步」的漂亮假象,但你得知道它本质不脱离事件循环!


🔍 自查题补充(面试常考)

async function async1() {
  console.log('A');
  await async2();
  console.log('B');
}
async function async2() {
  console.log('C');
}
console.log('D');
async1();
console.log('E');

输出顺序?

D
A
C
E
B

📋 十、大厂专项面试题 & 自查 Checklist

💼 高频面试题

Q1:Promise 和 setTimeout 谁先执行?

✅ 微任务(Promise)先执行,因为它在当前宏任务结束后立刻触发。

Q2:为什么微任务不能异步执行?

✅ 因为微任务是“微小但关键”的任务,如 .then,必须确保它们在状态变更之后立即完成,才能维持同步语义。

Q3:一个 setTimeout 嵌套两个 Promise 会发生什么?
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 优化任务执行

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
八股文2026-08-27
定时器按顺序播放多个音频,切后台音频会乱
本身不会补触发,但 它会被系统级冻结 ——iOS Safari 后台完全停止计时,切回前台后 只补执行一次 (不是不补,而是"缺失的中间状态补不上")。间隔短的不会出大问题, 间隔长的会出现"音频流被压缩" ——切回来后本该播 5 分钟的间隙,实际只过了 3 分钟,结果整段对不上。 核心原则 :定时器只能用来"提醒一次", 真正决定"该不该播"的是绝对时间戳 。 setTimeout 在后台会发生什么(按平台) | 平台 | 后台行为...
面试
八股文2026-08-27
线上项目白屏的原因
白屏的本质是 渲染管线某一环断了 ——可能是资源、JS、接口、路由、样式、兼容性任一环节出问题。排查按"控制台 → 网络 → DOM → 环境"四步定位。 本质(前端类比) 把网页想成一栋楼: 白屏 ≠ 一定是同一种原因——这是面试想听的层次。 六大类原因(按出现频率) | 类别 | 典型表现 | 真实案例 | | : | : | : | | ① 资源加载失败 | DOM 是空的 | 入口 JS 404、CDN 挂了、CSS 阻塞 |...
面试
八股文2026-08-27
背景图就是 1MB 大图,怎么优化
1MB 大背景图优化分 三步 :压缩体积(10 30x)、按需加载(按设备/视口/网速)、渲染期优化(GPU 合成)。背景图跟 不同——它是 CSS,不走浏览器的原生懒加载机制,得手动处理。 背景图 vs 的关键区别(先讲清楚这个) 这是面试官想听的"针对性认知"——背景图不是普通图片,不能套通用方案。 三步优化(按优先级) 第 1 步:压缩体积(最重要,立竿见影) 1MB 的来源一般是这几种 ,对应解决方案: | 原始问题 | 体积来...
面试
八股文2026-08-27
项目里很多图片和视频,怎么优化
图片视频优化分 四层 :网络层(CDN/格式)、加载层(懒加载/预加载)、渲染层(解码/缓存)、业务层(按需/降级)。面试要把这四层都讲到位才算有体系。 四层优化模型 第 1 层:网络层(省钱、省时间) 核心目标:让资源体积小、让用户拿到资源快 | 手段 | 作用 | 关键点 | | : | : | : | | 图片格式 | WebP/AVIF 比 JPEG 小 25 50% | 兼容 fallback | | 视频格式 | H.265...
面试
八股文2026-08-20
Embedding 向量模型:从语义表示到相似度计算
前言 大模型「读懂」文字靠的是 token,但 token 之间只有离散的编号关系,模型并不知道「苹果」和「李子」在语义上很近。要让程序能「理解」两段文字的相似程度,必须先把文本映射成一个 高维向量 ,再用几何方法比较。这一步就是 Embedding。 本篇基于我本地 今天的真实代码,从语义表示讲到余弦相似度,并复盘几个踩过的真实坑。 一、为什么需要 Embedding 传统关键词检索是「字面匹配」: Embedding 做的是「语义匹...
面试
八股文2026-08-19
前端面试100题
前端面试 100 题 适用方向:中高级前端 / React / Vue / Next.js / Nuxt / TypeScript / 工程化 / 实时通信 / Electron / Node.js / AI 应用前端 使用方式:优先掌握“标准回答”,再练“面试官追问”,最后把“结合你的简历怎么答”组织成自己的项目故事。 说明 “结合你的简历怎么答”只使用你简历中已经出现的项目与技术事实。 “标准回答 / 追问”属于通用前端知识总结,用...
面试

评论

请登录后发表评论

去登录
加载评论中...

目录