JavaScript 事件循环(Event Loop)详解

158 阅读5分钟

javaScript 是一门单线程语言,这意味着它在同一时间只能执行一个任务。然而,在实际开发中我们却可以进行异步操作(如定时器、网络请求等),这是因为浏览器的多线程机制和JavaScript 的事件循环(Event Loop)。

本文将从进程与线程的基本概念出发,结合 JS 引擎的工作方式、异步编程机制、事件循环流程、宏任务与微任务的区别,以及 await 的行为特点,系统地梳理 JavaScript 是如何在单线程下实现高效异步执行的。


一、进程与线程:程序运行的基础单位

1. 进程(Process)

进程是操作系统分配资源的基本单位,程序运行的资源容器,如浏览器每个 Tab 都是一个进程。当我们打开一个应用程序(如微信),系统会为这个应用创建一个独立的进程。在这个进程中包含了程序运行所需的所有资源(内存、文件句柄、线程等)。

📌 示例:你在手机上打开微信,从启动到关闭的整个过程,就是一个“微信进程”。

2. 线程(Thread)

线程是进程中的一个更小的执行单位,负责执行具体的指令。线程还是进程内部的执行单元,浏览器通过多线程实现异步。一个进程可以包含多个线程,这些线程共享进程的资源,并且可以并发或并行执行。

📌 示例:在微信中聊天是一个线程,视频通话是另一个线程。

3. 浏览器中的进程与线程

当你在浏览器中打开一个新的 Tab 页面时,浏览器会为该页面创建一个独立的进程,并在该进程中创建多个线程来协同工作:

线程类型职责说明
JS 引擎线程解析并执行 JavaScript 代码
渲染线程解析 HTML 和 CSS,渲染页面结构和样式
HTTP 请求线程处理网络请求(如 AJAX)
插件线程执行插件内容(如 Flash)
定时器线程管理 setTimeoutsetInterval

⚠️ 注意:JS 引擎线程与渲染线程是互斥的。也就是说,当 JS 引擎正在执行代码时,页面不会重新渲染;反之亦然。这是为了防止数据竞争和渲染不一致的问题。


二、JavaScript 单线程与异步机制

1. JavaScript 是单线程的

V8 引擎在执行 JavaScript 代码时,默认只会开启一个主线程(即 JS 引擎线程)。因此,所有的同步代码会依次执行,而异步任务则会被交给浏览器的其他线程去处理。

2. 异步任务的执行原理

虽然 JS 是单线程的,但浏览器本身是多线程的。JS 引擎遇到异步任务(如 setTimeoutfetch)时,会将其交给相应的浏览器线程去执行:

  • setTimeout → 定时器线程
  • fetch / XMLHttpRequest → HTTP 请求线程
  • 用户交互事件 → UI 线程

当异步任务完成后,浏览器会将对应的回调函数放入相应的任务队列中,等待 JS 引擎线程空闲时再执行。


三、事件循环(Event Loop)详解

总结:JavaScript 是单线程的,遇到耗时代码会阻塞,所以设计了事件循环机制来解决问题。代码分为同步任务和异步任务:同步任务立即执行(如 console.log),异步任务又分为宏任务和微任务。微任务包括 Promise.thenMutationObserver 等,宏任务包括 setTimeoutsetInterval、I/O 操作等。执行顺序是:先执行所有同步代码,然后清空微任务队列,必要时进行 UI 渲染,最后执行一个宏任务,接着开启下一轮事件循环,这样反复执行直到所有任务完成。这样既能处理异步操作,又不会阻塞主线程。

事件循环是 JavaScript 实现异步编程的核心机制,它的主要作用是协调 JS 主线程与其他任务队列之间的协作关系。在事件循环中,当主线程执行完当前的同步任务后,会检查事件队列中是否有待处理的事件。如果有,主线程会取出事件并执行对应的回调函数。这个循环的过程被称为事件循环(Event Loop),它由主线程任务队列两部分组成。主线程负责执行同步任务,而异步任务则通过任务队列进行处理。这种机制保证了异步任务在适当的时机能够插入执行,从而实现了JavaScript的非阻塞异步执行。

事件循环的执行流程如下:

  1. 执行同步代码(Call Stack)

    • 同步任务直接进入调用栈执行。
  2. 清空微任务队列(Microtask Queue)

    • 包括 Promise.thenMutationObserverqueueMicrotask 等。
    • 微任务优先级高于宏任务。
  3. 渲染页面(仅浏览器环境)

    • 如果有 DOM 更新需求,浏览器会在当前循环周期后更新视图。
  4. 执行一个宏任务(Macrotask Queue)

    • 包括 setTimeoutsetIntervalI/O 操作用户事件 等。
  5. 重复上述流程


四、宏任务 vs 微任务

类型常见示例特点
宏任务setTimeoutsetIntervalsetImmediateI/OUI 事件每次事件循环只执行一个宏任务
微任务Promise.thenMutationObserverqueueMicrotask在当前宏任务结束后立即执行,优先级更高

示例对比

javascript
深色版本
console.log("Start");

setTimeout(() => {
  console.log("Timeout");
}, 0);

Promise.resolve().then(() => {
  console.log("Promise");
});

console.log("End");

// 输出顺序:
// Start
// End
// Promise
// Timeout

五、await 关键字对事件循环的影响

1. await 的本质

await 是 async/await 语法的一部分,它的核心作用是:

  • 暂停异步函数的执行:当遇到 await 时,会暂停当前 async 函数的执行,直到等待的 Promise 状态变为 resolved
  • 语法糖:它本质上是 Promise.then() 的语法糖,但提供了更直观的同步编程风格。

2. await 对事件循环的具体影响

await 的执行会触发以下事件循环行为:

  • await 之前的代码:在 await 表达式之前的代码会同步执行(包括 await 右侧的表达式)。
  • await 之后的代码:await 之后的代码会被包装成一个微任务(microtask),并加入微任务队列。

示例分析

javascript
深色版本
async function test() {
  console.log("A");
  await Promise.resolve();
  console.log("B");
}

test();
console.log("C");

// 输出顺序:
// A
// C
// B

尽管 await 后面没有耗时操作,但 "B" 依然会在 "C" 之后输出,因为它被放到了微任务队列中。