🔥 深入理解 JavaScript 同步与异步:从 Event Loop 到 async/await
本文将带你彻底搞懂 JavaScript 的执行机制,掌握同步与异步的核心原理,以及如何使用 Promise 和 async/await 优雅地控制异步流程。
前言
作为一名前端开发者,你是否曾经困惑过:
- 为什么 JavaScript 需要异步?同步写完不行吗?
- 为什么
setTimeout的回调不会阻塞后续代码执行? - 为什么
Promise能让异步代码像同步一样优雅? - Event Loop 到底是什么?
今天,我们就来彻底搞懂这些问题!
一、进程与线程:理解并发的基础
在深入 JavaScript 之前,我们先来理解两个核心概念:
1.1 进程(Process)
进程是资源分配的基本单位。每个程序运行时,操作系统会为其分配独立的内存空间和系统资源。
1.2 线程(Thread)
线程是代码执行的基本单位。一个进程可以包含多个线程,它们共享进程的资源。
markdown
进程(董事长)── PID
└── 线程(经理)── TID
├── 主线程
└── 子线程
1.3 多线程 vs 单线程
| 特性 | 多线程(C++/Java) | 单线程(JavaScript) |
|---|---|---|
| 执行效率 | 高(并发执行) | I/O 密集型不低,CPU 密集型受限 |
| 复杂度 | 高(需要处理同步锁) | 低(简单易懂) |
| 适用场景 | 系统级应用 | 网页交互 |
JavaScript 被设计为单线程语言,这是因为网页交互场景相对简单,单线程足以应对,同时避免了复杂的线程同步问题。
二、JavaScript 的执行机制
2.1 代码执行流程
当我们运行一段 JavaScript 代码时:
- 启动进程:操作系统分配资源(PID)
- 创建主线程:JavaScript 引擎创建唯一的主线程
- 执行同步代码:快速执行完所有同步任务
- 处理异步任务:耗时任务交给浏览器的其他线程处理,完成后回调进入任务队列,由 Event Loop 调度执行
javascript
// 同步代码 sync
console.log('start');
// 异步代码 async(交给浏览器定时器线程,到期后回调进入任务队列)
setTimeout(() => {
console.log('222');
}, 1000);
console.log('end');
// 输出顺序:start → end → 222
2.2 同步与异步的本质区别
同步(Sync):代码按顺序执行,前一个任务完成才会执行下一个。
javascript
let a = 1;
let b = 2;
let c = 3;
console.log(a + b + c); // 6
异步(Async):耗时任务被跳过,先执行后续同步代码,等同步代码执行完后再处理。
2.3 为什么需要异步?
这是最关键的问题。我们先看一个场景:
javascript
// 假设网络请求需要 3 秒
const data = fetch('/api/users'); // 假设这是同步的
console.log(data);
// 在这 3 秒内,页面完全卡死:
// ❌ 用户点击按钮 ------ 没反应
// ❌ 动画停止播放
// ❌ 输入框无法输入
// ❌ 整个页面白屏或"未响应"
根本原因:JavaScript 是单线程的。
单线程意味着同一时刻只能做一件事。如果用同步方式处理耗时操作(网络请求、大量计算、定时器),主线程就会被阻塞,后续所有代码都得排队等着。
css
同步模式(阻塞):
主线程:[====网络请求 3 秒====][==后续代码==]
↑ 这 3 秒页面完全卡死
异步模式(非阻塞):
主线程:[注册回调][后续代码继续执行...]
浏览器网络线程: [====网络请求 3 秒====]
↓
任务队列: [回调入队]
↓
Event Loop: [取出回调执行]
异步的本质是:把耗时任务交给浏览器/Node.js 的其他线程处理,主线程继续往下执行,等耗时任务完成后再回来执行回调。
| 对比 | 同步处理网络请求 | 异步处理网络请求 |
|---|---|---|
| 用户体验 | 页面卡死 3 秒 | 页面流畅,数据回来后更新 |
| CPU 利用率 | 干等 3 秒,浪费 | 空闲时间可以做其他事 |
| 代码复杂度 | 简单,但不可用 | 稍复杂,但体验好 |
所以结论很简单:在单线程语言中,异步不是可选的,而是必须的。 如果 JavaScript 用同步方式处理所有事情,用户每次点击按钮、每次发请求,页面都会卡住------这显然是不可接受的。
三、Event Loop:异步的核心机制
3.1 什么是 Event Loop?
Event Loop(事件循环)是 JavaScript 处理异步任务的核心机制。它的工作流程:
javascript
┌───────────────────────────┐
│ 调用栈(Call Stack)│ ← 执行同步代码
└───────────┬───────────────┘
│ 调用栈清空后
▼
┌───────────────────────────┐
│ 微任务队列(Microtask) │ ← Promise.then、MutationObserver
│ 全部执行完 │
└───────────┬───────────────┘
│ 微任务队列清空
▼
┌───────────────────────────┐
│ 宏任务队列(Macrotask) │ ← setTimeout、setInterval、I/O
│ 取出一个执行 │
└───────────┬───────────────┘
│ 执行完回到顶部,重新检查微任务
▼
循环继续...
关键区别:
- 微任务(Microtask):优先级高,每个宏任务执行完后,会清空所有微任务
- 宏任务(Macrotask):优先级低,每次只取一个执行
3.2 微任务 vs 宏任务
JavaScript 的异步任务分为两类,执行优先级不同:
| 类型 | 常见 API | 执行时机 |
|---|---|---|
| 微任务(Microtask) | Promise.then/catch/finally、MutationObserver |
每个宏任务执行完后,清空所有微任务 |
| 宏任务(Macrotask) | setTimeout、setInterval、I/O、UI 渲染 |
每次 Event Loop 只取一个执行 |
执行顺序:同步代码 → 清空所有微任务 → 取一个宏任务执行 → 清空所有微任务 → 取一个宏任务 → ...
用一个例子来理解:
javascript
console.log('1'); // 同步
setTimeout(() => {
console.log('2'); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('3'); // 微任务
});
console.log('4'); // 同步
// 输出顺序:1 → 4 → 3 → 2
// 1. 同步代码:1、4
// 2. 清空微任务:3
// 3. 取一个宏任务:2
为什么 Promise 比 setTimeout 先执行? 因为 Promise 是微任务,优先级高于 setTimeout 这个宏任务。即使 setTimeout 延迟设为 0,也得等微任务队列清空后才能执行。
3.3 JavaScript 中的异步任务类型
- 定时器 :
setTimeout、setInterval - 网络请求 :
fetch、XMLHttpRequest - 事件监听 :
click、scroll等 DOM 事件 - Promise:ES6 引入的异步解决方案
四、Promise:异步编程的救星
4.1 为什么需要 Promise?
在 Promise 出现之前,我们只能通过回调函数处理异步:
javascript
// 回调地狱(Callback Hell)
getData(function(a) {
getMoreData(a, function(b) {
getEvenMoreData(b, function(c) {
// 嵌套越来越深...
});
});
});
Promise 的出现让异步代码变得优雅:
javascript
getData()
.then(a => getMoreData(a))
.then(b => getEvenMoreData(b))
.then(c => {
// 清晰的链式调用
})
.catch(err => {
// 统一的错误处理
});
4.2 Promise 的基本用法
创建 Promise
javascript
const p = new Promise((resolve, reject) => {
// executor 函数:立即执行,是耗时任务的容器
// 同步执行,内部可以容纳异步任务
setTimeout(() => {
// 异步任务成功完成
resolve('成功的结果');
// 或者异步任务失败
// reject('失败的原因');
}, 2000);
});
使用 Promise
javascript
p
.then((data) => {
console.log(data); // "成功的结果"
console.log('异步任务完成');
})
.catch((err) => {
console.log(err); // "失败的原因"
console.log('异步任务失败');
})
.finally(() => {
console.log('无论成功失败都会执行');
});
4.3 Promise 的三种状态
| 状态 | 说明 | 触发方式 |
|---|---|---|
| Pending | 进行中 | 初始状态 |
| Fulfilled | 已成功 | 调用 resolve() |
| Rejected | 已失败 | 调用 reject() |
重要特性:状态一旦改变,就不会再变!
javascript
const p = new Promise((resolve, reject) => {
resolve('第一次'); // 状态变为 Fulfilled
reject('第二次'); // 无效,状态已经固定
resolve('第三次'); // 无效,状态已经固定
});
// 只会执行第一个 then
p.then(data => console.log(data)); // "第一次"
五、async/await:更优雅的异步写法
async/await 是 ES2017 引入的语法糖,让异步代码看起来和同步代码一样简洁。
5.1 基本用法
javascript
// async 函数始终返回一个 Promise
async function getData() {
const response = await fetch('/api/users'); // 等待 Promise 完成
const users = await response.json(); // 再等待下一个 Promise
return users; // 自动包装为 Promise.resolve(users)
}
getData().then(users => console.log(users));
5.2 对比回调和 Promise
同一个需求,三种写法:
javascript
// ❌ 回调地狱
fetchUser(userId, function(user) {
fetchOrders(user.id, function(orders) {
fetchDetails(orders[0].id, function(detail) {
console.log(detail);
});
});
});
// ✅ Promise 链
fetchUser(userId)
.then(user => fetchOrders(user.id))
.then(orders => fetchDetails(orders[0].id))
.then(detail => console.log(detail));
// ✅✅ async/await(最直观)
async function showDetail() {
const user = await fetchUser(userId);
const orders = await fetchOrders(user.id);
const detail = await fetchDetails(orders[0].id);
console.log(detail);
}
5.3 错误处理
javascript
// 用 try/catch 捕获异步错误
async function fetchData() {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (err) {
console.log('请求失败:', err);
} finally {
console.log('请求结束');
}
}
5.4 并行执行
await 会阻塞后续代码,如果多个请求没有依赖关系,应该并行执行:
javascript
// ❌ 串行(总耗时 = 3 秒 + 2 秒 + 1 秒 = 6 秒)
const users = await fetch('/api/users'); // 3 秒
const posts = await fetch('/api/posts'); // 2 秒
const comments = await fetch('/api/comments'); // 1 秒
// ✅ 并行(总耗时 = max(3, 2, 1) = 3 秒)
const [users, posts, comments] = await Promise.all([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
]);
六、实战:手写 sleep 函数
JavaScript 原生没有 sleep 函数,但我们可以用 Promise 实现:
javascript
function sleep(t) {
return new Promise((resolve, reject) => {
console.log('同步执行');
setTimeout(() => {
resolve();
}, t);
});
}
// 使用
sleep(2000)
.then(() => {
console.log('2秒后执行');
});
6.1 执行流程解析
sleep(2000)被调用executor函数立即同步执行,打印 "同步执行"setTimeout交给浏览器定时器线程处理sleep返回一个 Pending 状态的 Promise- 2秒后,
setTimeout的回调执行,调用resolve() - Promise 状态变为 Fulfilled,触发
.then()回调
七、fetch 与 Promise
现代浏览器的 fetch API 底层就是基于 Promise:
javascript
console.log('start');
fetch("https://api.example.com/users", {
method: "POST",
})
.then((response) => {
console.log('请求成功:', response);
})
.catch((err) => {
console.log('请求失败:', err);
});
console.log('end');
// 输出顺序:
// 1. start
// 2. end
// 3. 请求成功/失败(取决于网络状况)
关键点 :fetch 是异步的,不会阻塞后续代码执行!
⚠️ 常见陷阱 :fetch 只有在网络错误 (如断网、DNS 失败)时才会触发 .catch()。HTTP 状态码 404、500 等不会触发 catch,需要手动判断:
javascript
fetch('/api/users')
.then(response => {
if (!response.ok) { // response.ok 为 true 表示状态码 200-299
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(err => console.log('请求失败:', err));
八、控制异步执行顺序
在实际开发中,我们经常需要控制多个异步任务的执行顺序。
8.1 顺序执行
javascript
// 需求:先获取所有用户,再获取每个用户的详情
fetch('/api/users')
.then(response => response.json())
.then(users => {
return fetch(`/api/users/${users[0].id}`);
})
.then(response => response.json())
.then(userDetail => {
console.log('用户详情:', userDetail);
});
8.2 并行执行
javascript
// 同时发起多个请求,全部完成后处理
Promise.all([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
])
.then(([users, posts, comments]) => {
console.log('所有数据:', users, posts, comments);
});
九、常见面试题解析
题目 1:代码输出顺序
javascript
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
答案 :1 → 4 → 3 → 2
解析(参考 3.2 节的微任务/宏任务知识):
- 同步代码优先执行:
1、4 - 清空微任务队列:
3(Promise.then 是微任务) - 取一个宏任务执行:
2(setTimeout 是宏任务)
题目 2:Promise 状态不可变
javascript
const promise = new Promise((resolve, reject) => {
resolve('success1');
reject('error');
resolve('success2');
});
promise
.then(res => console.log(res))
.catch(err => console.log(err));
答案 :success1
解析 :Promise 状态一旦改变就不可逆,第一个 resolve 后状态固定为 Fulfilled。
总结
| 概念 | 核心要点 |
|---|---|
| 进程 | 资源分配单位 |
| 线程 | 代码执行单位 |
| 为什么需要异步 | 单线程 + 同步阻塞 = 页面卡死,异步是必须的 |
| Event Loop | 同步代码 → 清空微任务 → 取一个宏任务 → 循环 |
| 微任务/宏任务 | Promise 是微任务(优先),setTimeout 是宏任务 |
| Promise | 异步编程解决方案,三种状态不可逆 |
| async/await | Promise 的语法糖,让异步代码像同步一样简洁 |
掌握这些核心概念,你就能从容应对各种异步编程场景!
参考资料
📝 作者 :ReBound 📅 日期 :2026年6月9日 🔖 标签:#JavaScript #异步编程 #Promise #EventLoop #前端开发
如果这篇文章对你有帮助,欢迎点赞、收藏、评论!你的支持是我创作的最大动力! 🚀