说到 JavaScript 的事件循环(Event Loop),很多人首先想到的往往是“单线程、异步、回调”。然而,真正保障任务一致性的核心并非单线程本身,而是那套严格且可预测的执行顺序规则——简单来说,它并非依赖“同时处理多件事”,而是将每一项任务的先后顺序安排得井井有条:谁先执行、谁后执行、谁必须等待谁,都清晰明确。

整个运行机制的核心可以概括为:先执行同步代码,然后一次性清空微任务队列,接着从宏任务队列中取出一个任务执行,如此循环往复。每个任务都严格按照队列顺序逐一执行,主线程独占资源,因此根本不存在并发冲突的问题。
同步代码优先执行,奠定确定性基础
所有同步语句严格按照书写顺序压入调用栈(Call Stack)并依次执行,过程中不会被任何异步回调打断。这确保了函数调用链、变量赋值、DOM 初始状态等关键路径完全可预测。例如:
console.log('a');
let x = 1;
console.log(x);
// 输出一定是 a → 1,不会被 setTimeout 或 Promise 打断
微任务总在宏任务结束后立即清空
每次宏任务(例如 setTimeout 回调、事件处理函数)执行完毕后,引擎会一次性、无中断地将当前所有微任务全部清空——包括在这次清空过程中新产生的微任务,比如 Promise.then 里再 new Promise 的回调。这种“清空式执行”彻底消除了微任务之间的相互穿插干扰:
- Promise 链的 then/catch 总是连续执行,不会被定时器或点击事件中途打断
- MutationObserver 回调在 DOM 变更后统一触发,不会与渲染或用户输入交错
- 即便在微任务中创建新的微任务,也会被纳入本轮清空范围,而不会推迟到下一轮
宏任务严格按队列顺序逐个取出
宏任务队列(包括 setTimeout、setInterval、I/O、UI 事件等)遵循先进先出(FIFO)原则,每次只取出一个任务执行。浏览器不会跳过、重排或并发执行宏任务:
- 两个 setTimeout(0) 按注册顺序排队,先注册的一定先执行
- 用户点击和键盘事件按触发时间顺序进入队列,不会因为某次处理耗时而被后发事件“超车”
- 即使某个宏任务执行时间很长(比如一个长循环),也不会影响其他宏任务的排队位置
主线程独占资源访问权,杜绝并发冲突
事件循环运行在唯一的主线程上,所有任务(包括回调)都共享同一内存空间和 DOM 环境。不存在多线程抢写变量或同时操作同一个元素的风险:
- 无需加锁机制,不会出现竞态条件(race condition)
- DOM 更新不会被另一个“线程”中途修改,渲染阶段看到的状态始终是任务执行后的最终结果
- Web Worker 虽然可以并行计算,但与主线程通信仍需通过 postMessage——进入主线程后,依然服从事件循环的调度
