Skip to content

5. 说一说 js 中的事件循环 ​

难度等级:🟡 中级 分类:JavaScript 归档日期:2026-10-27


其实事件循环应该不属于 JavaScript 语言本身。ECMAScript 规范定义 Job Queue(Promise 那部分),真正的事件循环规则由宿主环境定义。浏览器遵循 HTML 规范,Node 用的是 libuv,他们的行为有些许差异,但思想框架一致。

严格来说,完整的事件循环机制由宿主环境定义。ECMAScript 定义 Promise 等相关的 Job 机制及宿主接口;浏览器的事件循环由 HTML 规范定义,Node.js 则以 libuv 为基础。两者的具体行为有些许不同,但都通过调度任务来协调异步操作。

js 有一个先天的约束,是单线程的。可是他要处理的场景偏偏又充满等待,网络请求,定时器,用户鼠标键盘事件,文件读写等等。那问题就是一个线程,怎么处理那么多不知道什么时候才能完成的事情呢,关键还不能让页面卡住。事件循环就是为了处理这个问题。

通常来说,当前脚本中的同步代码会在调用栈中执行。执行结束,调用栈清空,通常会进行一次微任务队列检查,依次执行微任务,如果期间产生新的微任务,会加入队尾依次执行。

之后,浏览器会根据渲染时机决定是否更新页面,再继续处理后续任务。并不是每一个任务就一定渲染,要看渲染时机。

我们把这样一个过程叫做一个任务,如果在当前任务中,遇到类似定时器等以下宏任务。就把它加入任务队列,等待本次任务(包括同步代码,和微任务)完成,继续执行下一个任务。

常见的宏任务 ​

  • 整段 <script> 脚本的执行(页面里的第一个任务)
  • setTimeout / setInterval 的回调
  • 用户交互事件:点击、键盘、输入等
  • 网络相关回调:XHR 的 onload、fetch 底层的网络事件等
  • postMessage、MessageChannel
  • Node 中的 setImmediate、I/O 回调

常见的微任务 ​

  • Promise.then / catch / finally 的回调
  • await 之后的代码(本质上是 Promise 回调)
  • queueMicrotask():在库或框架代码中做“批量合并”。比如同一个任务内多次修改状态,只在最后统一处理一次,Vue 的 nextTick 就是类似思路,只是它用的是 Promise.then。
  • MutationObserver 的回调:监听 DOM 的变化,包括子节点增删、属性修改、文本内容变化
  • Node 中的 process.nextTick(它有自己的队列,比 Promise 微任务更早执行)。作用:把回调安排在“当前操作完成后、任何其他异步回调之前”执行。

例子 ​

javascript
let pending = false;
const queue = [];

function scheduleUpdate(job) {
  queue.push(job);
  if (!pending) {
    pending = true;
    queueMicrotask(flush);
  }
}

function flush() {
  pending = false;
  const jobs = queue.splice(0);
  jobs.forEach(job => job());
}
javascript
const observer = new MutationObserver((mutations) => {
  mutations.forEach(m => console.log(m.type, m));
});

observer.observe(document.body, {
  childList: true,   // 监听子节点增删
  attributes: true,  // 监听属性变化
  subtree: true,     // 包括所有后代节点
});

// 不用时记得断开
observer.disconnect();

个人面试题归档,持续更新中