深入理解JavaScript「事件循环机制」,彻底搞懂进程、线程与异步执行

564 阅读4分钟

在前端开发中,JavaScript 的事件循环(Event Loop)机制一直是面试和实际开发中的高频考点。很多初学者对进程、线程、同步、异步、微任务、宏任务等概念容易混淆。本文将结合具体例子,深入浅出地讲清楚这些核心知识点,帮助你彻底掌握事件循环的本质。


进程与线程:浏览器的幕后英雄

进程是操作系统分配资源的最小单位。比如你在手机上打开微信,这个动作就启动了一个进程,从打开到关闭,微信都在这个进程中运行。

线程是进程中的更小单位。一个进程可以包含多个线程,各司其职。例如微信的聊天界面需要渲染线程来显示内容,同时还需要网络线程来接收新消息。

在浏览器中,每打开一个新的 Tab 页面,实际上就是启动了一个新的进程。每个进程内部又有多个线程协同工作,比如:

  • HTTP 线程:负责网络请求
  • JS 引擎线程:负责执行 JavaScript 代码
  • 渲染线程:负责页面渲染

需要注意的是,JS 引擎线程和渲染线程是互斥的,即同一时刻只能有一个在工作,避免页面渲染和 JS 执行互相干扰。


JavaScript 的单线程与异步

JavaScript 设计之初就是单线程,即同一时刻只能有一个任务在执行。这是因为 JS 主要用于与页面交互,如果多线程同时操作 DOM,容易引发混乱。

但现代 Web 应用需要处理大量异步任务,比如网络请求、定时器等。为了解决这个问题,JavaScript 引入了异步机制,即把耗时操作交给浏览器其他线程处理,等结果出来后再通知 JS 引擎线程处理。


事件循环(Event Loop):JS 的调度员

事件循环是 JS 实现异步的核心机制。它的执行流程如下:

  1. 执行同步代码,遇到异步任务(如 setTimeout、Promise)时,将其放入任务队列。
  2. 同步代码执行完毕后,先执行微任务队列(如 Promise.then、MutationObserver、process.nextTick)。
  3. 微任务全部执行完毕后,渲染页面(如有必要)。
  4. 渲染后,执行宏任务队列(如 setTimeout、setInterval、ajax、DOM 事件),然后开启下一轮事件循环。

任务队列的分类

  • 微任务(Microtask):Promise.then、process.nextTick、MutationObserver、async/await ...
  • 宏任务(Macrotask):setTimeout、setInterval、ajax、DOM 事件 等...

具体例子解析

让我们通过几个具体的 JS 代码例子,来直观理解事件循环的执行顺序。

例子一:同步与异步的基本执行顺序

console.log(1);
setTimeout(() => {
  console.log(2);
}, 0);
console.log(3);

输出结果:

image.png 解析:

  • console.log(1) 立即执行,输出 1。
  • setTimeout 是宏任务,回调被放入宏任务队列,等待主线程空闲时执行。
  • console.log(3) 继续执行,输出 3。
  • 同步代码执行完毕后,事件循环开始,执行宏任务队列中的回调,输出 2。

例子二:微任务与宏任务的优先级

console.log('start');
setTimeout(() => {
  console.log('timeout');
}, 0);
Promise.resolve().then(() => {
  console.log('promise');
});
console.log('end');

输出结果:

image.png

解析:

  • startend 是同步代码,先输出。
  • setTimeout 回调进入宏任务队列。
  • Promise.then 回调进入微任务队列。
  • 同步代码执行完毕后,先清空微任务队列,输出 promise
  • 最后执行宏任务队列,输出 timeout

例子三:async/await 的本质

async function test() {
  console.log('a');
  await Promise.resolve();
  console.log('b');
}
test();
console.log('c');

输出结果:

image.png

解析:

  • test() 执行,输出 a
  • await 后面的代码(console.log('b'))会被放入微任务队列。
  • console.log('c') 是同步代码,立即执行。
  • 同步代码执行完毕后,执行微任务队列,输出 b

补充说明:
await 实际上会将后续代码“挤入”微任务队列,浏览器对 await 的处理甚至比普通 Promise.then 更加优先。


例子四:微任务与宏任务的嵌套

console.log('A');
setTimeout(() => {
  console.log('B');
  Promise.resolve().then(() => {
    console.log('C');
  });
}, 0);
Promise.resolve().then(() => {
  console.log('D');
});
console.log('E');

输出结果:

image.png

解析:

  • AE 是同步代码,先输出。
  • Promise.thenD 进入微任务队列。
  • setTimeout 的回调进入宏任务队列。
  • 同步代码执行完毕,先执行微任务队列,输出 D
  • 然后执行宏任务队列,输出 B,并在宏任务回调中又插入一个微任务(C)。
  • 当前宏任务执行完毕后,立即执行新插入的微任务,输出 C

五、总结与复习要点

  1. 进程是资源分配的最小单位,线程是执行的最小单位。
  2. 浏览器每个 Tab 是一个进程,内部有多个线程协作。
  3. JavaScript 是默认单线程,异步任务通过事件循环机制实现。
  4. 事件循环的执行顺序:同步代码 → 微任务队列 → 渲染 → 宏任务队列。
  5. 微任务优先于宏任务,Promise.then、async/await 都属于微任务。

JavaScript 的事件循环机制是理解异步编程的核心。掌握进程、线程、同步、异步、微任务、宏任务等概念,并通过具体例子来加深理解,希望本文能成为你复习和查漏补缺的好帮手!