JavaScript 异步编程完全指南:从回调地狱到 async/await,一次通关
你也许每天都在用
Promise和async/await,但闭眼能说清事件循环、微任务、宏任务的执行顺序吗?
为什么setTimeout明明是 0 毫秒,却要等到最后才执行?
更重要的是:日常开发中到底该用 Promise 还是 async/await?
本文从零开始,手写代码、画图讲解、逐层进阶,带你彻底掌握 JavaScript 异步编程,并给出实战最佳实践。
目录
- 为什么需要异步?
- 回调函数:最朴素的异步方案
- Promise:拯救回调地狱
- Promise 静态方法与链式技巧
- async/await:用同步的写法写异步
- 事件循环:微任务 vs 宏任务
- 实战建议:开发中该用 Promise 还是 async/await?
- 手写一个简易 Promise(进阶)
- 总结与面试题挑战
1. 为什么需要异步?
JavaScript 是单线程语言,同一时间只能做一件事。如果某个任务耗时很长(比如网络请求、文件读取、定时器),整个程序就会卡住,用户界面无法点击。为了避免这种情况,JS 把耗时操作设计成异步:先不等待结果,继续执行后面的代码,等结果回来了再处理。
生活类比:
- 同步(阻塞):你去食堂打饭,排队时只能干等,直到轮到你。
- 异步(非阻塞):你点完餐拿到号,先去占座刷手机,听到叫号再去取餐。
2. 回调函数:最朴素的异步方案
回调函数就是把一个函数作为参数传给另一个函数,当异步操作完成时调用这个函数。
2.1 定时器回调
console.log('开始');
setTimeout(() => {
console.log('2秒后执行');
}, 2000);
console.log('结束');
// 输出:开始 → 结束 → (2秒后)2秒后执行
2.2 模拟网络请求
function fetchUser(callback) {
setTimeout(() => {
const user = { id: 1, name: '张三' };
callback(user);
}, 1000);
}
fetchUser((user) => {
console.log(user); // { id: 1, name: '张三' }
});
2.3 回调地狱(Callback Hell)
当需要串行执行多个异步操作时,回调层层嵌套,可读性极差:
getUser(1, (user) => {
getOrders(user.id, (orders) => {
getProducts(orders[0].id, (products) => {
console.log(products);
});
});
});
这就是“回调地狱”,横向发展,错误处理困难,难以维护。
3. Promise:拯救回调地狱
Promise 是一个代表了未来某个结果的对象,它有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。状态只能改变一次,且一旦改变就不可逆。
3.1 创建 Promise
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve('成功的数据');
} else {
reject('失败原因');
}
}, 1000);
});
promise
.then((data) => console.log(data))
.catch((err) => console.error(err));
resolve:将状态从 pending → fulfilled,并传递结果。reject:将状态从 pending → rejected,并传递错误原因。
3.2 把回调函数改写成 Promise
function getUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id === 1) resolve({ id: 1, name: '张三' });
else reject(new Error('用户不存在'));
}, 500);
});
}
function getOrders(userId) {
return new Promise((resolve) => {
setTimeout(() => resolve([{ id: 101, name: '订单A' }]), 500);
});
}
// 链式调用
getUser(1)
.then(user => {
console.log(user);
return getOrders(user.id);
})
.then(orders => {
console.log(orders);
})
.catch(err => console.error(err));
3.3 链式调用的关键
then 方法总是返回一个新的 Promise,因此可以无限链下去。
- 如果
then的回调返回一个普通值,新 Promise 会以该值 resolve。 - 如果返回一个 Promise,新 Promise 会等待这个 Promise 完成。
- 如果抛出异常,新 Promise 会 reject。
Promise.resolve(1)
.then(x => x + 1) // 返回 2
.then(x => { throw new Error('出错了'); })
.catch(err => console.log(err));
4. Promise 静态方法与链式技巧
4.1 Promise.resolve() 与 Promise.reject()
快速创建已成功/失败的 Promise。
Promise.resolve('直接成功').then(console.log);
Promise.reject('直接失败').catch(console.log);
4.2 Promise.all():等待所有完成
并行执行多个 Promise,全部成功才成功,任一失败则失败。
const p1 = fetch('/api/user');
const p2 = fetch('/api/orders');
Promise.all([p1, p2])
.then(([user, orders]) => console.log(user, orders))
.catch(err => console.error(err));
4.3 Promise.race():取最快的一个
const timeout = new Promise((_, reject) => setTimeout(() => reject('超时'), 3000));
const request = fetch('/api/data');
Promise.race([request, timeout])
.then(data => console.log(data))
.catch(err => console.log(err));
4.4 Promise.allSettled():等待所有结束(无论成功失败)
const promises = [
Promise.resolve('成功'),
Promise.reject('失败'),
Promise.resolve('也成功')
];
Promise.allSettled(promises).then(results => {
results.forEach(result => console.log(result.status));
});
4.5 Promise.any():任意一个成功即成功(全部失败才失败)
Promise.any([
Promise.reject('错误1'),
Promise.resolve('成功'),
Promise.reject('错误2')
]).then(console.log); // 输出 '成功'
5. async/await:用同步的写法写异步
async 函数返回一个 Promise,await 等待 Promise 完成。它让异步代码变得像同步一样直观。
5.1 基本用法
async function fetchData() {
const user = await getUser(1);
const orders = await getOrders(user.id);
console.log(orders);
}
fetchData();
5.2 错误处理:try/catch
async function fetchData() {
try {
const user = await getUser(1);
const orders = await getOrders(user.id);
console.log(orders);
} catch (err) {
console.error('出错了', err);
}
}
5.3 并行执行:不要滥用 await
下面这个例子是串行,总耗时 = 两个请求时间之和:
const user = await getUser(1);
const orders = await getOrders(user.id); // 必须等 user 完成
如果要并行,使用 Promise.all:
const [user, posts] = await Promise.all([getUser(1), getPosts(1)]);
5.4 顶层 await(ES2022)
在模块中可以直接使用 await,不需要包裹 async 函数。
<script type="module">
const data = await fetch('/api/data').then(res => res.json());
console.log(data);
</script>
6. 事件循环:微任务 vs 宏任务
光会用 Promise 还不够,要理解执行顺序,才能避开面试陷阱。
6.1 宏任务(MacroTask)和微任务(MicroTask)
- 宏任务:整体代码块、
setTimeout、setInterval、I/O、UI 渲染、setImmediate(Node.js) - 微任务:
Promise.then/catch/finally、MutationObserver、queueMicrotask、process.nextTick(Node.js)
事件循环流程:
- 执行一个宏任务(第一次是全局脚本)。
- 执行当前宏任务产生的所有微任务(清空微任务队列)。
- 必要时渲染页面。
- 取出下一个宏任务执行,重复。
6.2 经典示例
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
输出:1 4 3 2
解释:同步代码(1,4)先执行 → 清空微任务(3)→ 宏任务(2)
6.3 async/await 与事件循环
async function foo() {
console.log('2');
await bar();
console.log('4');
}
async function bar() {
console.log('3');
}
console.log('1');
foo();
console.log('5');
输出:1 2 3 5 4
关键:await bar() 后面的代码(console.log('4'))相当于被 Promise.resolve(bar()).then(() => console.log('4')) 包裹,所以进入微任务队列。
7. 实战建议:开发中该用 Promise 还是 async/await?
很多新手面对 Promise 的 then、catch、finally、all、race… 觉得方法太多记不住。其实你完全不用担心,日常开发中 90% 的场景用 async/await 就够了。
7.1 对比:Promise vs async/await
| 对比 | Promise | async/await |
|---|---|---|
| 写法 | 链式调用 .then(),容易嵌套 | 像同步代码一样从上往下写 |
| 错误处理 | .catch() 容易漏或位置不对 | try/catch,和 Python/Java 一样 |
| 调试 | 断点不好打,堆栈信息有时乱 | 断点打在 await 行,清晰直观 |
| 组合异步 | Promise.all 仍然需要,但可以 await 它 | 复杂流程仍需 Promise 静态方法 |
7.2 你只需要记住这几个 Promise 方法就够了
| 方法 | 场景 | 记忆点 |
|---|---|---|
async/await | 代替 then/catch | 日常写异步就用它 |
Promise.all | 并发请求,全部成功才算成功 | “等所有人都到齐” |
Promise.race | 超时控制(谁快用谁) | “赛跑,第一名决定结果” |
Promise.resolve / reject | 快速返回一个已完成/失败的 Promise | 偶尔用于测试或封装 |
其他方法(allSettled、any、finally)遇到具体需求时再查,不用硬记。
7.3 正确的学习与使用路径
-
先掌握
async/awaitasync function getData() { try { const res = await fetch('/api/user'); const data = await res.json(); console.log(data); } catch (err) { console.error(err); } }这能覆盖 80% 的异步场景。
-
再学
Promise.allconst [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);这是并发请求的标准写法。
-
最后了解
Promise.race(超时控制)const timeout = new Promise((_, reject) => setTimeout(() => reject('超时'), 3000)); const data = await Promise.race([fetchData(), timeout]);
绝大多数前端开发者不需要自己实现 Promise 或手写
then链。
业务代码中用async/await+try/catch+Promise.all就够了。
面试考手写 Promise 是为了考察底层理解,但不代表工作中要那样写。
所以,现在你可以放心地用 async/await 写所有异步逻辑,只在需要并发或竞赛时借助 Promise.all / Promise.race。这样你只需要记两个关键字(async、await)和一个方法(Promise.all),负担直接减半。
8. 手写一个简易 Promise(进阶)
理解 Promise 原理,面试加分项。下面实现一个简化版(不处理链式异步返回 Promise 等高级情况,但足够说明核心)。
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
const promise2 = new MyPromise((resolve, reject) => {
if (this.state === 'fulfilled') {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (err) {
reject(err);
}
}, 0);
} else if (this.state === 'rejected') {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (err) {
reject(err);
}
}, 0);
} else {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (err) {
reject(err);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (err) {
reject(err);
}
}, 0);
});
}
});
return promise2;
}
catch(onRejected) {
return this.then(null, onRejected);
}
static resolve(value) {
return new MyPromise(resolve => resolve(value));
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
}
使用示例:
const p = new MyPromise((resolve, reject) => {
setTimeout(() => resolve('hello'), 1000);
});
p.then(console.log);
9. 总结与面试题挑战
9.1 核心知识点回顾
- 异步:不阻塞主线程,通过回调、Promise、async/await 处理。
- Promise:三种状态、链式调用、静态方法(all, race, allSettled, any)。
- async/await:同步写法,配合 try/catch 处理错误,是日常开发首选。
- 事件循环:微任务先于宏任务,
Promise.then是微任务,setTimeout是宏任务。 - 实战建议:用
async/await+Promise.all覆盖绝大多数场景,不必死记所有 Promise API。
9.2 挑战题(评论区留下你的答案)
async function test() {
console.log(1);
await Promise.resolve();
console.log(2);
}
console.log(3);
test();
console.log(4);
new Promise((resolve) => {
console.log(5);
resolve();
}).then(() => console.log(6));
输出顺序是什么?(答案:3 1 5 4 2 6)
如果你能解释清楚上述输出,并能在日常开发中熟练运用 async/await + Promise.all,异步这一关你就彻底过了。下一篇我们将进入事件循环深度解析或原型与 class,你想先看哪个?评论区告诉我~