调用栈清空是事件循环启动下一轮调度的唯一前提,事件循环只在栈为空时检查任务队列;宏任务执行前必须栈空,微任务在每次栈空后立即全量执行完毕;ui渲染作为宏任务,需等前一宏任务及所有微任务完成后才执行。

调用栈清空不是事件循环的“开关”,而是它启动下一轮调度的唯一前提。事件循环本身不主动运行,只在调用栈彻底为空时才开始检查任务队列。
调用栈清空是宏任务执行的必要条件
每当一个宏任务(如 setTimeout 回调、script 整体代码、UI 渲染)要开始执行,主线程必须先确保调用栈完全为空。哪怕只剩一个 console.log 正在执行,事件循环也不会取下一个宏任务。
- 同步代码执行完后,栈变为空 → 触发微任务清空 → 再触发宏任务取队
- 宏任务执行过程中产生的新宏任务(比如又调了 setTimeout),会进队但不会打断当前执行
- 即使多个 setTimeout 同时到期,也得等前一个宏任务彻底结束、栈再次清空后,才轮到下一个
微任务在每次栈空后立即全量执行
微任务(Promise.then、queueMicrotask 等)不排队等待下一轮事件循环,而是在每一次宏任务结束后、下个宏任务开始前,被一次性全部执行完毕。
- 哪怕在微任务执行中又创建了新的微任务,也会被追加进当前微任务队列,并继续执行,直到队列彻底为空
- 微任务之间没有“中断点”,它们构成一个连续执行块
- 这个机制保证了 Promise 链总比同轮 setTimeout 更早输出,无论延时设为多少
UI 渲染也遵循宏任务节奏
浏览器把页面重绘、样式更新等渲染操作当作一个特殊的宏任务,它不会插队,只能等前一个宏任务 + 所有后续微任务都完成之后,才可能被执行。
- 这意味着:同步代码 → 微任务 → 渲染 → 下一个宏任务
- 如果某个宏任务执行时间过长(比如万级循环),渲染就会被卡住,用户看到“页面冻结”
- 想让界面及时响应,就得主动切分长任务,留出栈空间隙供渲染和用户交互介入
事件循环从不主动抢占,只被动响应
事件循环不是独立线程,也不是定时器,它只是宿主环境(浏览器或 Node.js)提供的协调逻辑——只在两个固定时机介入:全局脚本结束、每个宏任务结束。这两次的共同特征就是调用栈为空。
- 它不看“时间到了没”,只看“栈空了没”
- 异步回调何时执行,取决于它注册在哪一轮栈清空之后
- 所谓“0 毫秒定时器”,实际延迟由当前任务负载决定,可能几十毫秒甚至更久











