先记住这个答案
浏览器中,当前任务里的同步代码先执行。在微任务检查点,运行时会持续处理微任务,直到队列为空,包括期间新加入的微任务。随后浏览器才有机会更新渲染并调度后续任务。Promise 的回调通常是微任务,setTimeout 的回调是计时条件满足后才可调度的任务。
- Promise 执行器同步运行
- 微任务检查点会清空队列
- 渲染有机会发生,不保证每轮发生
宏任务和微任务,区别在哪里?
面试里常说的“宏任务”对应浏览器模型中的 task,例如脚本执行、计时器回调和用户交互相关任务。浏览器可以有不同任务来源与队列,不能把所有异步操作想成一条全局先进先出的队列。
Promise 的 then/catch/finally 回调以及 queueMicrotask 回调属于微任务。微任务不会打断正在执行的同步 JavaScript;到检查点时,队列里的微任务会持续执行。Promise 构造器接收的执行器函数则是同步调用的,不要因为外面写了 new Promise 就把里面的代码全部归为异步。
Promise 和 setTimeout 执行顺序:一道题走完整个过程
在浏览器控制台运行下面这段代码。先记录同步输出,再按入队顺序处理微任务,最后考虑定时器回调。注意 promise 回调内部又创建了一个微任务。
验证“JavaScript 事件循环”中的“Promise 和 setTimeout 执行顺序:一道题走完整个过程”时,应固定输入、运行环境与触发顺序,并同时记录正常结果和失败结果。这样能确认本节结论来自目标机制,而不是缓存、旧状态或偶然时序。
console.log('start');
setTimeout(() => console.log('timer'), 0);
Promise.resolve().then(() => {
console.log('promise');
queueMicrotask(() => console.log('nested'));
});
queueMicrotask(() => console.log('microtask'));
console.log('end');查看输出与解释
start
end
promise
microtask
nested
timer同步阶段打印 start、end;初始微任务队列是 promise、microtask。执行 promise 时把 nested 加到队尾,因此它排在 microtask 后面。微任务处理完,timer 才有机会运行。
- 同步阶段
- setTimeout 只安排后续回调,Promise.then 和 queueMicrotask 登记微任务,当前脚本继续执行到 end。
- 检查点
- promise 先执行并新增 nested;队列随后依次执行 microtask、nested。不是只处理进入检查点时已有的那些任务。
- 定时器阶段
- timer 输出最后。延时为 0 也不意味着立即执行,它仍需要等待主线程可用和后续任务调度。
微任务跑完后,页面一定立刻渲染吗?
不一定。浏览器会根据渲染机会、刷新节奏和页面状态决定是否更新渲染。回答时用“有机会渲染”更准确,不能把每个任务结束机械等同于一次绘制,也不能把渲染简单列成普通宏任务。
如果微任务持续给自己追加微任务,检查点可能长时间无法结束,后续任务和渲染也会被拖延。把大量计算包进 Promise 并不能让它自动离开主线程。可按工作量拆分任务,或把适合的计算交给 Web Worker。
async / await 怎样放进 Event Loop 模型?
调用 async 函数时,遇到首个 await 之前的代码会同步运行。await 会暂停该函数后续部分,待所等待的值兑现后,再通过异步续执行继续运行;即使等待的是已兑现的 Promise,也不会在当前同步调用里直接跑完 await 后面的代码。
分析题目时先写出同步路径,再标出 Promise 何时兑现和续执行何时排入微任务。复杂的 thenable 同化和 Promise 链会引入额外作业,不要用“每个 await 固定等于一轮”这样的口诀替代实际分析。
容易答错的地方
- “所有 Promise 里的代码都是微任务”
- 执行器同步运行,反应回调异步调度。应具体指出是哪一段函数体,不要把整个 Promise 视作一个微任务。
- “setTimeout(fn, 0) 必定 4ms 后执行”
- 延时只是调度条件。HTML 的 4ms 下限与多层嵌套计时器有关,不是所有首次调用都固定 4ms;繁忙主线程和后台策略还会造成额外延迟。
- “浏览器与 Node.js 的规则可以混着背”
- 两者都涉及任务调度与微任务,但 Node.js 还涉及自己的阶段和 process.nextTick。先声明运行环境,再讨论该环境里的顺序。
面试官还会怎么问?
为什么 Promise 的回调比 setTimeout(fn, 0) 先输出?
对于本页示例,已兑现 Promise 的 then 回调会进入微任务队列,在当前任务之后的检查点被处理。定时器回调要等后续任务调度。这不是任意 Promise 都必定早于任意定时器:若 Promise 还未兑现,回调可能根本还没入队。
在微任务中继续创建微任务,会留到下一轮吗?
不会因为是新加入的就自动延后到下一轮任务。检查点持续取出微任务直至队列为空,因此新微任务也会在同一次检查点处理。这也是无界递归微任务可能阻塞交互的原因。
requestAnimationFrame 属于宏任务还是微任务?
它是浏览器渲染更新流程中的动画帧回调,不能简单当作普通任务队列或微任务队列的一项来排顺序。它通常用于下一次重绘前更新动画状态,实际调用也受页面可见性和渲染调度影响。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。