Event Loop 面试通关:从原理到口述再到实战

Event Loop 面试通关:从原理到口述再到实战

一、readme.md 知识点梳理

readme.md 围绕 事件循环(Event Loop) 展开,核心可归纳为以下五层:

第一层:单线程困境

"每个页面都有一个渲染进程,启动一个主线程,负责的任务特别多,而且还是单线程。"

JS 是单线程语言(一个调用栈),同一时刻只能做一件事。如果同步等待耗时操作(网络/定时器),页面会卡死。这是 Event Loop 诞生的根本原因。

第二层:消息队列 + 事件循环

"渲染主线程会频繁接收到来自于 IO 线程的一些任务......以消息的方式。"

渲染主线程既要解析 DOM、计算样式、布局、绘制,又要执行 JS。其他线程(网络进程、IO 线程)通过消息队列投递任务,主线程用 Event Loop 轮询处理。

第三层:两种队列

宏任务 (Macro Task) 微任务 (Micro Task)
来源 setTimeoutsetInterval、I/O、UI 渲染、<script> 整体 Promise.thenMutationObserverasync/awaitqueueMicrotask
执行策略 每次只取 一个 执行 每次 全部清空(包括执行中新产生的)
优先级 高(插入在宏任务之间)

第四层:一轮 Event Loop 的完整流程

javascript 复制代码
执行一个宏任务(script 就是第一个)
    ↓
清空所有微任务(Promise.then、await 后续、MutationObserver...)
    ↓
(可选)UI 渲染 / 动画帧 / 垃圾回收
    ↓
取下一个宏任务 ← 回到顶部

第五层:关键结论

"同步代码优先执行 → 微任务插队执行(优先级高于宏任务) → 最后才轮到宏任务。"


二、面试话术(逐句可用)

当面试官问:"说说你对 Event Loop 的理解"

标准回答模板:

JavaScript 是单线程的,为了防止耗时任务阻塞主线程,浏览器引入了事件循环机制。任务分为同步和异步,异步又分为宏任务和微任务。一个宏任务执行完后,会把微任务队列全部清空,然后再取下一个宏任务,形成循环。

宏任务包括 script 整体代码、setTimeout、setInterval、I/O 和 UI 渲染;微任务包括 Promise.then、MutationObserver、await 后续代码和 queueMicrotask。

执行顺序是:同步代码 → 所有微任务 → 一个宏任务 → 所有微任务 → 下一个宏任务,以此类推。

当面试官问:"宏任务和微任务的区别是什么?"

有三个核心区别:

第一,来源不同:宏任务由宿主环境(浏览器/Node)提供,微任务由 JS 引擎自身(Promise、MutationObserver)产生。

第二,执行策略不同 :每次事件循环只执行一个 宏任务,但会一次性清空微任务队列,包括执行过程中新产生的微任务。

第三,优先级不同:微任务优先级更高------每执行完一个宏任务,都会立即清空微任务队列,然后才可能进行 UI 渲染,再取下一个宏任务。

当面试官问:"async/await 在 Event Loop 中是怎么执行的?"

async 函数在遇到 await 之前的代码是同步 执行的。await 后面的表达式也是同步求值的,但 await 之后的代码会被包装成 微任务 放入队列,效果等价于 Promise.resolve().then(() => { ... })

因此 await 后的代码不会立即执行,而是要等当前同步代码执行完、并且在当前微任务队列中排队等待。

当面试官问:"为什么 MutationObserver 是微任务而不是宏任务?"

因为 DOM 变化的响应需要尽快执行。如果 MutationObserver 是宏任务,两次 DOM 修改之间可能插入其他宏任务甚至 UI 渲染,导致观测延迟和数据不一致。放在微任务队列中可以保证在同一次事件循环内、下一个宏任务之前就拿到所有变更。


三、1.html 执行顺序分析

代码回顾

html 复制代码
<script>
    console.log('同步代码 1');                          // ①

    setTimeout(() => {                                 // → 宏任务1
        console.log('setTimeout 1');
        Promise.resolve().then(() => {
            console.log('setTimeout 1 内部微任务');
        });
    }, 0);

    const promise1 = new Promise((resolve) => {
        console.log('Promise 构造函数');                // ②
        resolve();
        console.log('Promise 构造函数内 resolve 后');   // ③
    });

    promise1.then(() => {                              // → 微任务A
        console.log('Promise.then 1');
        setTimeout(() => {                             // → 宏任务3
            console.log('Promise.then 1 内部 setTimeout');
        }, 0);
    });

    async function asyncFn() {
        console.log('async 函数同步部分');              // ④
        await Promise.resolve();                       // → 后续作为微任务B
        console.log('await 后微任务');
    }
    asyncFn();

    console.log('同步代码 2');                          // ⑤

    queueMicrotask(() => {                             // → 微任务C
        console.log('queueMicrotask 微任务');
    });

    const observer = new MutationObserver(() => {       // → 微任务D
        console.log('MutationObserver 微任务');
    });
    const div = document.createElement('div');
    observer.observe(div, { attributes: true });
    div.setAttribute('data-test', '1');
</script>

分步推演

阶段一:同步代码(第一个宏任务 <script>
序号 代码 行为
console.log('同步代码 1') 打印 同步代码 1
--- setTimeout(..., 0) 回调进入 宏任务队列(记为 MT-1),不执行
new Promise(executor) executor 是同步的:打印 Promise 构造函数
--- resolve() Promise 状态变为 resolved,.then 回调进入 微任务队列(记为 mT-A)
executor 内 resolve 后 打印 Promise 构造函数内 resolve 后
asyncFn() 调用 进入函数,打印 async 函数同步部分
--- await Promise.resolve() Promise.resolve() 同步完成;await 将后续代码作为微任务入队(记为 mT-B)
console.log('同步代码 2') 打印 同步代码 2
--- queueMicrotask(...) 回调进入微任务队列(记为 mT-C)
--- div.setAttribute(...) 触发 MutationObserver,回调进入微任务队列(记为 mT-D)

同步阶段输出:

javascript 复制代码
同步代码 1
Promise 构造函数
Promise 构造函数内 resolve 后
async 函数同步部分
同步代码 2
阶段二:清空微任务队列

微任务队列(FIFO):mT-A → mT-B → mT-C → mT-D

顺序 微任务 打印内容
mT-A (promise1.then) Promise.then 1
--- 内部 setTimeout(..., 0) 回调进入宏任务队列(记为 MT-2)
mT-B (await 后续) await 后微任务
mT-C (queueMicrotask) queueMicrotask 微任务
mT-D (MutationObserver) MutationObserver 微任务
阶段三:执行下一个宏任务 MT-1
顺序 代码 打印内容
MT-1: setTimeout 1 setTimeout 1
--- Promise.resolve().then(...) 回调进入微任务队列(记为 mT-E)

MT-1 结束 → 立即清空微任务:

顺序 微任务 打印内容
mT-E setTimeout 1 内部微任务
阶段四:执行下一个宏任务 MT-2
顺序 代码 打印内容
MT-2: Promise.then 1 内部 setTimeout Promise.then 1 内部 setTimeout

最终输出(完整顺序)

javascript 复制代码
同步代码 1
Promise 构造函数
Promise 构造函数内 resolve 后
async 函数同步部分
同步代码 2
Promise.then 1
await 后微任务
queueMicrotask 微任务
MutationObserver 微任务
setTimeout 1
setTimeout 1 内部微任务
Promise.then 1 内部 setTimeout

一张图总结整个流程

xml 复制代码
<script> 宏任务开始
│
├─ 同步代码 1                    ← 打印 ①
├─ setTimeout 注册              → MT-1 入队
├─ Promise 构造函数              ← 打印 ②
├─ Promise 构造函数内 resolve 后  ← 打印 ③
├─ promise1.then 注册           → mT-A 入队
├─ async 函数同步部分             ← 打印 ④
├─ await 后续                   → mT-B 入队
├─ 同步代码 2                    ← 打印 ⑤
├─ queueMicrotask 注册          → mT-C 入队
├─ MutationObserver 注册        → mT-D 入队
│
├─ ★ 清空微任务 ★
│   ├─ mT-A: Promise.then 1     ← 打印 ⑥  | MT-2 入队
│   ├─ mT-B: await 后微任务      ← 打印 ⑦
│   ├─ mT-C: queueMicrotask     ← 打印 ⑧
│   └─ mT-D: MutationObserver   ← 打印 ⑨
│
├─ ★ 取下一个宏任务 MT-1 ★
│   ├─ setTimeout 1              ← 打印 ⑩
│   └─ ★ 清空微任务 ★
│       └─ mT-E: 内部微任务      ← 打印 ⑪
│
└─ ★ 取下一个宏任务 MT-2 ★
    └─ 内部 setTimeout           ← 打印 ⑫

面试点睛:这个例子中的三个坑

  1. new Promise 的 executor 是同步的 ------Promise 构造函数resolve 后 都在同步阶段打印,很多人误以为 Promise 全都是异步。

  2. await 之后是微任务 ------async 函数同步部分 是同步打印的,但 await 后微任务 排队到了微任务阶段,比 Promise.then 1 还晚(因为 await 注册时机在 promise1.then 之后)。

  3. MutationObserver 和 queueMicrotask 同级竞争------它们都是微任务,谁先注册谁先执行。这里 queueMicrotask 在前,MutationObserver 在后,但在同一个微任务清空轮次内依次执行。

相关推荐
kyriewen1 小时前
手写 call、apply、bind:从原理到实现,附 3 个最容易忽略的边界情况
前端·javascript·面试
用户2181697049301 小时前
swift (三) 枚举 结构体 类
前端
胡萝卜术2 小时前
从内存视角重新认识 JavaScript 数据类型:一份深度学习笔记
前端·javascript·面试
IVEN_2 小时前
记一次诡异的前端白屏故障:Nginx Proxy Cache 内存缓存"幽灵"事件
前端·nginx
如果超人不会飞2 小时前
TinyRobot SuggestionPills紧凑的建议按钮组组件
前端·vue.js
如果超人不会飞2 小时前
TinyRobot Container构建优雅的AI对话容器
前端·vue.js
Waay2 小时前
K8s ETCD 详解|备份恢复+静态Pod原理+kubectl查询底层流程(面试必考)
面试·kubernetes·etcd
幸运小圣2 小时前
全面解析 Web 核心性能指标:LCP、INP、CLS 是什么、怎么用、怎么看
前端
如果超人不会飞2 小时前
TinyRobot SuggestionPopover智能建议弹出框组件
前端·vue.js