真实面经题目 · 原创解析

了解 JS 的异步嘛?从单线程、事件循环、宏任务和微任务讲一下

这题考察的不是背宏任务、微任务名词,而是能否从调用栈、宿主异步、任务队列、微任务检查点和渲染机会推导代码执行顺序,并解释页面为什么会被长任务或微任务链卡住。

出现于:携程 · 前端

60 秒回答模板

JS 在一个主线程上执行调用栈,同一时刻只能跑一段 JS。计时器、网络、DOM 事件这类异步能力由浏览器等宿主环境处理,完成后把回调放回任务队列。事件循环每次取一个 task 执行,等同步栈清空后进入 microtask checkpoint,把 Promise.then、queueMicrotask、await 后续等微任务清空;之后浏览器才有机会做样式、布局和绘制,再进入下一轮 task。所以顺序题要按同步代码、微任务、下一个宏任务推,卡顿题要看长同步任务和微任务是否把渲染机会饿住。

考点 核心机制与工程取舍
难度 中高频面试题
回答目标 按定义、机制、场景讲清楚

深入解析

01

主线程与调用栈

JS 的执行模型先看调用栈:全局脚本、函数调用、回调最终都要回到同一个主线程栈里执行。所谓单线程不是浏览器只有一个线程,而是同一个 JS 执行上下文不能并行跑两段业务 JS,因此长函数会挡住后面的点击回调、定时器回调和渲染。

02

宿主异步能力

setTimeout、网络请求、DOM 事件监听不是靠 JS 栈一直等待。JS 把任务注册给宿主环境;宿主在计时到期、IO 完成或事件触发后,把对应回调排进 task 队列,等待主线程空出来再执行。

03

Task 与 microtask checkpoint

面试里说的宏任务更准确地对应浏览器事件循环里的 task,例如初始 script、timer、用户交互事件。每个 task 跑完、调用栈清空后,会进行一次 microtask checkpoint,按先进先出执行 Promise reaction、queueMicrotask、MutationObserver 等微任务,并继续执行微任务里新追加的微任务,直到队列为空。

04

Promise、setTimeout、async/await 顺序

顺序题按三步推:先跑同步代码,包括 Promise 构造器和 async 函数 await 前的部分;再清空微任务,包括 then 回调和 await 后续;最后才轮到到期 timer 这类下一个 task。

05

渲染机会与微任务饥饿

浏览器通常在一个 task 和随后 microtask checkpoint 完成后,才有机会执行 requestAnimationFrame、样式计算、布局和绘制;但渲染不是每轮事件循环都保证发生。如果同步 JS 很长,或微任务里不断 Promise.then 递归追加,主线程会一直到不了渲染和下一轮 task。

06

浏览器与 Node 边界

前端题默认讲浏览器模型:task、microtask、渲染机会。被追问 Node 时再补充 Node 有 timers、poll、check 等阶段,process.nextTick 又有更高优先级;不要用 Node 的 nextTick 顺序解释浏览器里的 Promise 和 setTimeout。

易错点

  • 把 new Promise 里的执行器当成微任务;实际它同步执行,then/catch/finally 回调才进入微任务。
  • 认为 await 后面的代码会立刻同步执行;实际 async 函数在 await 处分段,await 后续进入微任务。
  • 把 setTimeout(fn, 0) 理解成 0 毫秒后立刻插队执行,忽略当前 task 和全部微任务必须先结束。
  • 以为每轮只执行当前已有微任务;实际 microtask checkpoint 会继续清空执行过程中新增的微任务。
  • 说宏任务之后一定渲染一次;浏览器只是获得渲染机会,是否真的绘制还受主线程、帧率和页面状态影响。
  • 把浏览器事件循环和 Node 的 process.nextTick、poll、check 阶段混在一起答。

面试官追问

混合代码怎么推输出顺序?

先把全局 script 当作当前 task:同步输出先发生,Promise 构造器同步,async 函数 await 前同步;then 和 await 后续排入微任务;当前 task 结束后清空微任务;最后才执行 setTimeout 这种下一轮 task。

await 前后分别什么时候执行?

async 函数调用后会立即执行到第一个 await,await 右侧表达式也会先求值;await 后面的代码会被拆成 Promise continuation,进入微任务队列。

setTimeout(fn, 0) 是不是马上执行?

不是。0 只表示达到最小延迟后可以排队,回调仍要等当前 task 的同步栈结束、微任务队列清空,并排在已有可执行 task 之后。

为什么大量 Promise.then 会让页面卡住?

microtask checkpoint 要一直清到队列为空,微任务里继续追加微任务会让事件循环长期停在 checkpoint,浏览器到不了渲染机会,也处理不了后续点击和 timer。

事件循环和浏览器渲染是什么关系?

一个 task 执行完并清空微任务后,浏览器才有机会进入渲染流程;但如果主线程忙、帧预算不够或页面被节流,这次机会可能被推迟。