JavaScript执行链路由调用栈、微任务队列和宏任务队列协同运作:同步代码入栈执行,栈空后立即清空微任务队列,再取下一个宏任务,形成事件循环闭环。

JavaScript 的执行链路不是线性排队,而是一套协同运转的机制:同步代码走调用栈,异步任务按优先级分流到宏任务队列或微任务队列,事件循环负责调度它们的执行时机。理解这三者如何配合,才能真正看懂为什么 Promise.then 总比 setTimeout 先执行,也才能避开“明明写了 await 却没按预期等待”的陷阱。
调用栈:同步代码的执行现场
调用栈是 JavaScript 执行同步逻辑的唯一场所,遵循先进后出(LIFO)原则。每当函数被调用,它就被压入栈顶;函数返回,就从栈顶弹出。栈一旦为空,说明当前宏任务的同步部分已执行完毕——这是触发后续微任务执行的关键信号。
常见误区是认为 console.log 或变量赋值“不占栈”,其实只要在主线程里运行,就一定经过调用栈。哪怕只有一行 let a = 1;,它也是栈中一个帧。
- 栈满会报
RangeError: Maximum call stack size exceeded(如递归无出口) - 异步回调(如
setTimeout回调)本身不进当前栈,而是等被取出后,作为新宏任务重新入栈执行 - await 后的代码,实际是被编译成 Promise 链,属于微任务,不直接入当前调用栈
微任务:当前宏任务结束后的“加急处理”
微任务不是“小一点的宏任务”,而是在调用栈清空后、下一个宏任务开始前,必须全部执行完的一批高优任务。它的核心特性是“清空式执行”——只要队列里还有微任务,引擎就不会去取宏任务。
立即学习“Java免费学习笔记(深入)”;
典型微任务包括:Promise.then/catch/finally、queueMicrotask、MutationObserver 回调(浏览器)、process.nextTick(Node.js)。注意:Promise.resolve().then() 注册的是微任务,但 Promise 构造函数内部的同步代码仍是同步执行。
- 嵌套的
Promise.then会形成链式微任务,全部在本轮清空,不会跨轮次 -
queueMicrotask是最纯粹的微任务 API,行为与Promise.then一致,但无 Promise 状态管理开销 - 微任务中抛出未捕获错误,会作为本轮微任务执行结束后的异常,可能触发
unhandledrejection
宏任务:事件循环的主干节奏
宏任务是事件循环的基本调度单位,每次只取一个执行。整个 <script> 标签内容本身就是第一个宏任务;之后由宿主环境(浏览器或 Node.js)不断推入新宏任务,比如定时器到期、用户点击、网络响应、I/O 完成等。
常见宏任务有:setTimeout/setInterval、I/O 操作、UI 渲染(浏览器)、postMessage、setImmediate(Node.js)、事件监听回调(如 click)。
- 即使设为
setTimeout(fn, 0),也必须等到当前宏任务+所有微任务执行完,才能进入下一轮宏任务队列 - UI 渲染在浏览器中是一个隐式宏任务,通常发生在微任务清空之后、下一个宏任务之前(但受帧率限制,不一定每轮都发生)
- 多个
setTimeout会按注册顺序排队,但执行时机还受系统计时精度和任务负载影响
执行链路:一次事件循环的完整闭环
一次标准事件循环迭代包含四个不可跳过的环节:执行一个宏任务 → 清空所有微任务 → (浏览器)尝试 UI 渲染 → 取下一个宏任务。这个闭环不断重复,构成 JS 的运行节拍。
关键在于“清空微任务”发生在宏任务之间,而非之内。也就是说,宏任务内部产生的微任务,会在该宏任务结束时立即执行,而不是等到外部其他宏任务跑完。
- 同步代码属于当前宏任务的一部分,优先执行
- 宏任务内注册的微任务,归入当前轮次的微任务队列,不会延迟到下一轮
- 微任务中再注册新的微任务(如
Promise.then里又调Promise.resolve().then),也会被加入同一轮微任务队列并继续执行,直到队列为空 - 渲染不是 JavaScript 引擎控制的,而是浏览器在微任务清空后自主决定是否绘制,因此不能用
setTimeout精确等待 DOM 更新完成


















