🚀 JavaScript 异步编程完全指南
从零彻底搞懂同步、异步、Event Loop、Promise 和 async/await
📌 为什么需要异步?
CPU 执行时间不能霸占一个任务太久,操作系统以几十毫秒为单位轮询分配给各个进程。
- 🚫 如果 JS 没有异步机制,遇到网络请求、定时器这类耗时任务时,整个页面就会 "卡死"等待,用户无法做任何操作
- ✅ 异步的存在,让 JS 可以把耗时任务 "扔到一边",先执行后面的代码,等耗时任务完成了再回来处理
💡 生活类比:你正在做饭(同步任务),突然快递到了(异步任务)。你不会傻站在门口等快递,而是继续做饭,等门铃响了再去取。异步就是这种 "不阻塞" 的智慧!
🧵 进程与线程
- 🏢 进程:公司,有独立资源(PID process id),进程之间相互隔离
- 👷 线程:员工,在进程内干活(TID thread id),同一进程内的线程共享资源
- 🎯 主线程:JS 唯一的执行线程,负责所有同步代码
- 🔧 子线程(浏览器提供) :处理定时器、I/O、UI 事件等异步任务
- ⚠️ JS 本身只有一个主线程,子线程由浏览器/Node 提供,JS 无法直接创建或操作子线程
📊 线程模型示意图
bash
JS 是单线程语言
├── 👑 主线程:跑所有 JS 代码(同步任务)
└── 🔧 子线程(由浏览器/Node/bun 提供,JS 本身不直接操作)
├── ⏰ 定时器线程(setTimeout/setInterval)
├── 🌐 I/O 线程(网络请求、文件读写)
└── 🖱️ 事件触发线程(鼠标点击等 UI 事件)
- ⚙️ C++ / Java 等系统级别语言有多进程架构,执行效率高,但手动管理线程很复杂,容易出 bug
- ✨ JS 设计为单线程,天然避免了 **"多线程竞争"**和 "死锁" 等复杂问题
🤔 JS 能异步执行代码的原因
- 📜 JS 本身是单线程语言,同一时间只能做一件事,代码从上往下顺序执行
- 🌐 但浏览器/Node/bun 提供了多个 子线程,分别处理定时器、I/O、UI 事件等异步任务
- 🤝 主线程负责执行同步代码,子线程负责处理异步任务,两者配合实现 "异步" 效果
⚠️ 注意事项 :JS 的 "异步" 本质是一种 模拟出来的并发,不是真正的多线程并行。子线程是浏览器/Node 提供的底层能力,JS 代码本身始终运行在单线程上。
📋 JS 有哪些异步任务
| 异步任务 | 说明 | 常见 API |
|---|---|---|
| ⏲️ setTimeout(延时器) | 延迟指定时间后执行一次代码 | setTimeout(fn, ms) |
| 🔁 setInterval(定时器) | 每隔指定时间重复执行代码 | setInterval(fn, ms) |
| 📡 I/O 操作 | 网络请求、文件读写等耗时操作 | fetch、XMLHttpRequest、fs.readFile |
| 🖱️ UI 事件 | 用户与页面交互产生 | click、mouseover、keydown 等事件监听 |
💡 fetch 与 Promise :
fetch是浏览器内置的 API,用于发送网络请求。它的底层基于 Promise 实现,fetch()返回的就是一个 Promise 对象,所以可以直接用.then()和.catch()处理请求结果或错误。
⚠️ 注意事项:
- ⏰
setTimeout的延迟时间是最短等待时间,不是精确时间,因为要等同步代码执行完才能轮到它- 🚀
setTimeout(fn, 0)并不是立即执行,而是 "尽快执行",依然要等当前同步代码全部跑完
🔄 JS 执行机制(Event Loop)
- 🚀 执行前端 script 或 node / bun 代码时,系统会启动一个进程,拥有独立的 PID,负责分配资源
- 👑 进程启动一个 主线程,JS 因为足够简单,只有单线程
📍 执行顺序(六步法)
- 🏃♂️ 先把所有 同步任务 快速执行掉,让用户看到页面
- 👀 遇到定时器、fetch 请求、事件等耗时性的 异步任务,JS 不会原地等待
- 📝 把这些异步任务注册到 event loop(事件循环)中,跳过它们,先继续执行后面的同步代码
- ⏳ 等同步代码全部执行完毕后,再到 event loop 中把 已完成的异步任务 拿出来执行回调
- 🔁 重复上述过程 ------ 这就是 Event Loop(事件循环)
🎯 宏任务与微任务
异步任务还可以进一步分为两类,它们的执行优先级不同:
| 类型 | 特点 | 常见 API | 示例 |
|---|---|---|---|
| 🏗️ 宏任务(Macrotask) | 每次 Event Loop 只取一个执行 | setTimeout、setInterval、UI 事件 |
定时器回调 |
| ⚡ 微任务(Microtask) | 每次清空队列中 所有 微任务 | Promise.then/.catch/.finally |
Promise 回调 |
📐 执行顺序规则
erlang
同步代码 → ⚡ 清空所有微任务 → 🏗️ 取一个宏任务 → ⚡ 清空所有微任务 → 🏗️ 取下一个宏任务...
💡 为什么 Promise 的回调比 setTimeout 先执行?
因为 Promise 的回调是 微任务,优先级比 setTimeout(宏任务)高。 即使 setTimeout 延时为 0,它也要等当前所有微任务执行完才轮到。
⚠️ 注意事项:
- 🔒 同步代码永远比异步代码先执行,即使
setTimeout延时为 0 也一样- 🥇 微任务执行顺序优先于宏任务 ,所以在同一个代码块中,Promise 的
.then()会比setTimeout的回调先执行- 🔁 如果微任务中又产生了新的微任务,会继续清空,不会等宏任务,可能导致 "微任务死循环" 阻塞页面
🎨 经典面试题
javascript
console.log('1️⃣ 同步代码开始');
setTimeout(() => {
console.log('5️⃣ setTimeout 宏任务');
}, 0);
Promise.resolve().then(() => {
console.log('3️⃣ Promise 微任务');
});
console.log('2️⃣ 同步代码结束');
// 输出顺序:1️⃣ → 2️⃣ → 3️⃣ → 5️⃣
🎮 控制执行流程
实际开发中经常遇到 "先做 A,再用 A 的结果做 B" 的场景:
- 📱 场景举例:先调用 A 接口获取所有用户列表,再根据用户列表逐个调用 B 接口获取详情
- 😵 如果没有好的异步控制机制,代码会写成 "回调地狱"(层层嵌套),难以维护
- 🎁 Promise 的出现就是为了解决这个问题,让异步代码写起来像同步一样清晰
🏚️ 回调地狱示例(反面教材)
javascript
// 😱 噩梦般的回调地狱
getUser(function(user) {
getOrders(user.id, function(orders) {
getOrderDetails(orders[0].id, function(details) {
getProductInfo(details.productId, function(product) {
console.log(product);
// 再嵌套下去就要崩溃了...
});
});
});
});
💖 理解 Promise
Promise 是 ES6 引入的异步编程解决方案,用来解决回调地狱问题,让异步任务的控制流程更清晰。
🏗️ 基本概念
- 📦 实例化 Promise :
new Promise(executor) - ⚙️ executor(执行函数) :
- 创建 Promise 时 立即执行(同步的)
- 它是耗时性任务的容器,里面可以写异步代码(如
setTimeout、fetch) - 会自动接收到两个函数参数:
resolve和reject
🎚️ 三种状态
Promise 实例有且仅有以下三种状态,一旦状态改变就不可再变:
| 状态 | 图标 | 含义 | 触发方式 | 对应回调 |
|---|---|---|---|---|
pending |
⏳ | 进行中(初始状态) | 刚 new Promise 时 | --- |
fulfilled |
✅ | 已成功 | 调用 resolve(value) |
.then() |
rejected |
❌ | 已失败 | 调用 reject(reason) |
.catch() |
🔗 链式调用
- 🟢
resolve(result)--- 任务成功解决,把结果传给.then() - 🔴
reject(err)--- 任务失败,把失败原因传给.catch() - 🔚
.finally()--- 无论成功失败都会执行,适合做收尾操作(如关闭加载动画)
📝 语法细节
javascript
const p = new Promise((resolve, reject) => {
// executor 在这里写耗时任务
// resolve(值) → 让 Promise 变为 fulfilled 状态
// reject(错误) → 让 Promise 变为 rejected 状态
setTimeout(() => {
const success = true;
if (success) {
resolve('🎉 数据加载成功');
} else {
reject('💥 出错了');
}
}, 1000);
});
p.then(值 => {
console.log('成功:', 值);
})
.catch(错误 => {
console.log('失败:', 错误);
})
.finally(() => {
console.log('🏁 执行完毕,清理工作');
});
⚠️ 注意事项:
- ⚡ executor 是 同步执行 的,不是异步的!它里面包含的
setTimeout、fetch等才是异步代码- 🛑
resolve和reject只能调用其中一个,且只能调用一次。先调用的生效,后面的忽略- 🔄
.then()和.catch()返回的都是新的 Promise 实例,所以可以无限链式调用- 🚨 如果没有
.catch()捕获错误,rejected 状态的 Promise 会抛出一个 未捕获的异常,可能导致程序崩溃- 🧹
.finally()不接受任何参数,无法知道前面是成功还是失败
✨ Promise 解决回调地狱
javascript
// 🎉 优雅的 Promise 链式调用
getUser()
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => getProductInfo(details.productId))
.then(product => console.log(product))
.catch(error => console.error('❌ 出错了:', error));
🎭 async / await --- Promise 的语法糖
async/await 是 ES2017 引入的语法,本质上是 Promise 的 语法糖,目的是让异步代码写起来更像同步代码,进一步提高可读性。
📖 基本用法
- 🏷️
async:加在函数前面,表示这个函数是异步函数,它 会自动返回一个 Promise - ⏸️
await:只能在async函数内部使用,后面跟一个 Promise 对象,会 等待这个 Promise 完成 ,然后直接拿到resolve的值
javascript
async function 获取数据() {
const 结果 = await fetch('https://api.example.com/user'); // 等待 fetch 完成,拿到结果
const 数据 = await 结果.json(); // 等待解析完成
console.log(数据); // 拿到最终数据
return 数据;
}
💡 用
async/await替代.then()链,代码看起来就像从上往下执行的同步代码一样,更容易理解和维护。
🆚 对比示例
javascript
// 🔴 Promise 写法
function getUserData() {
fetch('/api/user')
.then(response => response.json())
.then(data => {
console.log('用户数据:', data);
return fetch(`/api/orders?userId=${data.id}`);
})
.then(response => response.json())
.then(orders => console.log('订单数据:', orders))
.catch(error => console.error('错误:', error));
}
// 🟢 async/await 写法(更清晰!)
async function getUserData() {
try {
const userResponse = await fetch('/api/user');
const user = await userResponse.json();
console.log('用户数据:', user);
const ordersResponse = await fetch(`/api/orders?userId=${user.id}`);
const orders = await ordersResponse.json();
console.log('订单数据:', orders);
} catch (error) {
console.error('错误:', error);
}
}
⚠️ 注意事项:
- 🚫
await只能在async函数内部使用,在外面用会报语法错误- 🔒
await会阻塞它所在的async函数内的后续代码执行,但不会阻塞整个主线程(函数外的代码照常执行)- 📦
async函数本身返回的是一个 Promise,所以也可以用.then()和.catch()- 🥷 用
try...catch可以捕获await中的异常,替代.catch()
📚 总结速查表
| 概念 | 核心要点 |
|---|---|
| 🧵 单线程 | JS 只有一个主线程,异步靠浏览器子线程配合 |
| 🔄 Event Loop | 同步 → 清空微任务 → 取一个宏任务 → 循环 |
| ⚡ 微任务 | Promise.then/catch/finally,优先级高 |
| 🏗️ 宏任务 | setTimeout/setInterval/UI 事件,优先级低 |
| 💖 Promise | 解决回调地狱,三种状态:pending/fulfilled/rejected |
| 🎭 async/await | Promise 语法糖,让异步代码像同步一样写 |
🎉 恭喜你! 读完这篇文章,你已经彻底掌握了 JavaScript 异步编程的核心知识。从 Event Loop 到 Promise,再到 async/await,现在你可以自信地处理任何异步场景了!
📝 如果觉得文章对你有帮助,欢迎点赞 👍、收藏 ⭐、评论 💬 支持一下~