题目重点:从同步栈、Promise/queueMicrotask 与 setTimeout 的队列关系推导输出,区分浏览器与 Node 的宿主差异。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 训练示例运行在支持 Promise、queueMicrotask、setTimeout 的现代 JavaScript 宿主;一次顶层同步调用结束后,Promise 和 queueMicrotask 按入队先后执行,timer 在后续任务阶段运行。
- 题目未指明浏览器或 Node、定时器延迟、嵌套任务及渲染行为;不能将示例输出说成原面试代码答案。
解题路线
- 先把所有同步语句列出,执行到栈空;遇到 setTimeout 只登记 timer,遇到 Promise.then/queueMicrotask 只排微任务,不立刻执行回调。
- 同步栈清空后按 FIFO 排空微任务队列;微任务内若再排微任务,通常在进入下一个任务前继续排空。随后处理有资格运行的 timer;`setTimeout(fn,0)` 不保证墙钟时间为零。
- 若题中有 `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()` 后面的语句落在哪个队列?
- 无限微任务递归对浏览器渲染有什么影响?
