javaScript 是一门单线程语言,这意味着它在同一时间只能执行一个任务。然而,在实际开发中我们却可以进行异步操作(如定时器、网络请求等),这是因为浏览器的多线程机制和JavaScript 的事件循环(Event Loop)。
本文将从进程与线程的基本概念出发,结合 JS 引擎的工作方式、异步编程机制、事件循环流程、宏任务与微任务的区别,以及 await 的行为特点,系统地梳理 JavaScript 是如何在单线程下实现高效异步执行的。
一、进程与线程:程序运行的基础单位
1. 进程(Process)
进程是操作系统分配资源的基本单位,程序运行的资源容器,如浏览器每个 Tab 都是一个进程。当我们打开一个应用程序(如微信),系统会为这个应用创建一个独立的进程。在这个进程中包含了程序运行所需的所有资源(内存、文件句柄、线程等)。
📌 示例:你在手机上打开微信,从启动到关闭的整个过程,就是一个“微信进程”。
2. 线程(Thread)
线程是进程中的一个更小的执行单位,负责执行具体的指令。线程还是进程内部的执行单元,浏览器通过多线程实现异步。一个进程可以包含多个线程,这些线程共享进程的资源,并且可以并发或并行执行。
📌 示例:在微信中聊天是一个线程,视频通话是另一个线程。
3. 浏览器中的进程与线程
当你在浏览器中打开一个新的 Tab 页面时,浏览器会为该页面创建一个独立的进程,并在该进程中创建多个线程来协同工作:
| 线程类型 | 职责说明 |
|---|---|
| JS 引擎线程 | 解析并执行 JavaScript 代码 |
| 渲染线程 | 解析 HTML 和 CSS,渲染页面结构和样式 |
| HTTP 请求线程 | 处理网络请求(如 AJAX) |
| 插件线程 | 执行插件内容(如 Flash) |
| 定时器线程 | 管理 setTimeout、setInterval |
⚠️ 注意:JS 引擎线程与渲染线程是互斥的。也就是说,当 JS 引擎正在执行代码时,页面不会重新渲染;反之亦然。这是为了防止数据竞争和渲染不一致的问题。
二、JavaScript 单线程与异步机制
1. JavaScript 是单线程的
V8 引擎在执行 JavaScript 代码时,默认只会开启一个主线程(即 JS 引擎线程)。因此,所有的同步代码会依次执行,而异步任务则会被交给浏览器的其他线程去处理。
2. 异步任务的执行原理
虽然 JS 是单线程的,但浏览器本身是多线程的。JS 引擎遇到异步任务(如 setTimeout、fetch)时,会将其交给相应的浏览器线程去执行:
setTimeout→ 定时器线程fetch/XMLHttpRequest→ HTTP 请求线程- 用户交互事件 → UI 线程
当异步任务完成后,浏览器会将对应的回调函数放入相应的任务队列中,等待 JS 引擎线程空闲时再执行。
三、事件循环(Event Loop)详解
总结:JavaScript 是单线程的,遇到耗时代码会阻塞,所以设计了事件循环机制来解决问题。代码分为同步任务和异步任务:同步任务立即执行(如 console.log),异步任务又分为宏任务和微任务。微任务包括 Promise.then、MutationObserver 等,宏任务包括 setTimeout、setInterval、I/O 操作等。执行顺序是:先执行所有同步代码,然后清空微任务队列,必要时进行 UI 渲染,最后执行一个宏任务,接着开启下一轮事件循环,这样反复执行直到所有任务完成。这样既能处理异步操作,又不会阻塞主线程。
事件循环是 JavaScript 实现异步编程的核心机制,它的主要作用是协调 JS 主线程与其他任务队列之间的协作关系。在事件循环中,当主线程执行完当前的同步任务后,会检查事件队列中是否有待处理的事件。如果有,主线程会取出事件并执行对应的回调函数。这个循环的过程被称为事件循环(Event Loop),它由主线程和任务队列两部分组成。主线程负责执行同步任务,而异步任务则通过任务队列进行处理。这种机制保证了异步任务在适当的时机能够插入执行,从而实现了JavaScript的非阻塞异步执行。
事件循环的执行流程如下:
-
执行同步代码(Call Stack)
- 同步任务直接进入调用栈执行。
-
清空微任务队列(Microtask Queue)
- 包括
Promise.then、MutationObserver、queueMicrotask等。 - 微任务优先级高于宏任务。
- 包括
-
渲染页面(仅浏览器环境)
- 如果有 DOM 更新需求,浏览器会在当前循环周期后更新视图。
-
执行一个宏任务(Macrotask Queue)
- 包括
setTimeout、setInterval、I/O 操作、用户事件等。
- 包括
-
重复上述流程
四、宏任务 vs 微任务
| 类型 | 常见示例 | 特点 |
|---|---|---|
| 宏任务 | setTimeout、setInterval、setImmediate、I/O、UI 事件 | 每次事件循环只执行一个宏任务 |
| 微任务 | Promise.then、MutationObserver、queueMicrotask | 在当前宏任务结束后立即执行,优先级更高 |
示例对比
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" 之后输出,因为它被放到了微任务队列中。