Full Stack Intern · Frontend Onsite

Rippling 前端:Microtask 与 Macrotask 的执行顺序

从同步栈、Promise/queueMicrotask 与 setTimeout 的队列关系推导输出,区分浏览器与 Node 的宿主差异。

题目重点:从同步栈、Promise/queueMicrotask 与 setTimeout 的队列关系推导输出,区分浏览器与 Node 的宿主差异。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 训练示例运行在支持 Promise、queueMicrotask、setTimeout 的现代 JavaScript 宿主;一次顶层同步调用结束后,Promise 和 queueMicrotask 按入队先后执行,timer 在后续任务阶段运行。
  • 题目未指明浏览器或 Node、定时器延迟、嵌套任务及渲染行为;不能将示例输出说成原面试代码答案。

解题路线

  1. 先把所有同步语句列出,执行到栈空;遇到 setTimeout 只登记 timer,遇到 Promise.then/queueMicrotask 只排微任务,不立刻执行回调。
  2. 同步栈清空后按 FIFO 排空微任务队列;微任务内若再排微任务,通常在进入下一个任务前继续排空。随后处理有资格运行的 timer;`setTimeout(fn,0)` 不保证墙钟时间为零。
  3. 若题中有 `await`,把 await 后续当作微任务续体,并核对等待的 promise 何时 resolve;若有浏览器渲染、MessageChannel、Node `process.nextTick` 等,先声明宿主和事件循环阶段。

手动走一遍

训练代码同步产生 sync-start、sync-end;随后先排入的 Promise 回调产生 promise,再由 queueMicrotask 产生 microtask;最后 timer 产生 timer。返回前用 10ms timer 等待,因此得到五项完整轨迹。

JavaScript 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

export async function traceEventLoop() {
  const events = [];
  events.push("sync-start");
  setTimeout(() => events.push("timer"), 0);
  Promise.resolve().then(() => events.push("promise"));
  queueMicrotask(() => events.push("microtask"));
  events.push("sync-end");
  await new Promise((resolve) => setTimeout(resolve, 10));
  return events;
}

复杂度

仅预测固定片段的空间 O(排队回调数);事件循环并非按代码行数一次排序,回调可继续入队。单次回调执行成本取决于自身工作,过长微任务链会阻塞下一任务与渲染。

易错点与追问

容易失分的边界

  • 看到 setTimeout(...,0) 就让它先于微任务。
  • 把 Promise.then 当同步调用。
  • 忽略微任务中再次入队的回调。
  • 将 Node 专用 nextTick 规则直接套到浏览器。

继续深挖

  • 微任务内再次 queueMicrotask 会在 timer 前执行吗?
  • `await Promise.resolve()` 后面的语句落在哪个队列?
  • 无限微任务递归对浏览器渲染有什么影响?
✓已复制微信号!