javascript 异步执行由调用栈、微任务队列和宏任务队列协同调度:调用栈执行同步代码;宏任务(如 settimeout)每轮只执行一个,结束后立即清空微任务队列(如 promise.then);ui 渲染在微任务清空后、下一轮宏任务前发生。

JavaScript 代码执行不是线性排队那么简单,而是由调用栈、微任务队列和宏任务队列三者协同调度的结果。理解这三者的角色与协作顺序,才能真正看懂异步代码为什么按那个顺序运行。
调用栈:同步代码的执行现场
调用栈是 JavaScript 引擎执行同步代码的“工作台”。函数调用时入栈,返回时出栈,遵循后进先出(LIFO)规则。它不处理异步逻辑,只忠实执行当前在栈顶的函数。
一旦某个函数内部触发了异步操作(比如 setTimeout 或 Promise.resolve()),引擎不会卡在这里等结果,而是立刻继续往下执行栈中剩下的同步代码。
常见误区:以为 setTimeout(fn, 0) 就能“立刻执行”——其实它只是尽快被安排进宏任务队列,仍需等调用栈清空、当前宏任务结束、微任务清空之后才轮到它。
微任务:当前宏任务结束后的“加急处理”
微任务优先级高于宏任务,且每次宏任务执行完毕后,会一次性清空整个微任务队列,中间不穿插任何其他任务。
典型微任务包括:
- Promise.then/catch/finally 的回调
- queueMicrotask() 显式加入的任务
- MutationObserver(监听 DOM 变更)
- Node.js 中的 process.nextTick(优先级最高,但仅限 Node 环境)
注意:一个微任务内部再创建新的微任务(比如 then 里又调用 Promise.resolve().then(...)),新任务会追加到当前微任务队列末尾,仍会在本轮清空过程中执行完。
宏任务:事件循环的基本调度单元
宏任务构成事件循环的一次“迭代起点”,每次只取一个执行。执行完它,紧接着就是清空微任务队列,然后才进入下一轮宏任务。
常见宏任务包括:
- 页面加载时的 整体 script 标签代码
- setTimeout 和 setInterval 的回调
- I/O 回调(如 fetch 响应、文件读取完成)
- UI 渲染(浏览器环境,在微任务清空后、下一个宏任务前可能发生)
- requestAnimationFrame(严格来说属于渲染阶段,常被归为宏任务相关环节)
关键点:宏任务之间天然隔开,中间必然插入一次完整的微任务清空过程。这也是为什么多个 Promise.then 总比同层的 setTimeout 先输出。
执行流程:一个清晰的循环链条
一次典型的事件循环周期如下:
- 执行一个宏任务(例如主脚本或某个定时器回调)
- 执行过程中遇到同步代码,压入/弹出调用栈
- 遇到异步操作,将其回调按类型放入对应队列(微任务 or 宏任务)
- 该宏任务执行完毕,调用栈清空
- 立即检查并逐个执行所有待处理的微任务,直到微任务队列为空
- (浏览器)可选地进行 UI 渲染
- 从宏任务队列中取出下一个任务,开始新一轮循环
这个链条不是抽象概念,而是真实控制着 console.log 输出顺序、DOM 更新时机、甚至动画帧节奏的实际机制。











