你肯定遇到过这样的场景:调用接口获取用户信息后,还要用用户ID请求订单数据,结果写了一堆嵌套的回调函数——代码越写越乱,调试时找不到哪里出错。这就是“回调地狱”,而Promise和Async/Await正是为解决这个问题而生的。

一、先搞懂:为什么需要异步编程?
JavaScript是单线程语言,同一时间只能做一件事。如果同步执行一个耗时操作(比如请求接口、读取文件),页面会卡住,用户没法点击按钮、滚动页面——这显然不可行。异步编程的核心是“先注册任务,等结果回来再处理”,让主线程继续做其他事。
比如早期的回调函数写法:
// 回调函数版:获取用户信息后请求订单
function getUser(callback) {
setTimeout(() => {
callback({ id: 1, name: '张三' });
}, 1000);
}
function getOrders(userId, callback) {
setTimeout(() => {
callback([{ id: 101, goods: '手机' }]);
}, 1000);
}
// 嵌套回调:层级多了就变成“回调地狱”
getUser(user => {
getOrders(user.id, orders => {
console.log('用户订单:', orders); // 2秒后输出
});
});
这种写法的问题很明显:嵌套层级深、错误处理分散(每个回调都要写error判断)、可读性差。这时候就需要Promise出场了。
二、Promise:告别回调地狱的第一步
Promise是ES6引入的异步解决方案,它把异步操作包装成一个“容器”,用状态变化(pending→fulfilled/rejected)来管理结果。
1. Promise的基础用法
Promise的构造函数接收一个 executor 函数(立即执行),里面有两个参数:
– resolve
:将Promise状态从pending改为fulfilled(成功),并传递结果;
– reject
:将状态改为rejected(失败),传递错误信息。
用Promise改写上面的例子:
// 用Promise封装异步操作
function getUser() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: 1, name: '张三' }); // 成功时调用resolve
// reject(new Error('获取用户失败')); // 失败时调用reject
}, 1000);
});
}
function getOrders(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([{ id: 101, goods: '手机' }]);
}, 1000);
});
}
// 用then()处理成功,catch()处理失败
getUser()
.then(user => getOrders(user.id)) // 链式调用:用户信息成功后请求订单
.then(orders => console.log('用户订单:', orders)) // 2秒后输出
.catch(error => console.error('出错了:', error)); // 统一处理所有错误
这里的关键是链式调用:每个then()
返回一个新的Promise,所以能一直链下去。而catch()
会捕获链条中所有的错误——不管是getUser()
还是getOrders()
出错,都能在这里处理,比回调函数的分散处理方便多了。
2. Promise的核心API
除了then()
和catch()
,Promise还有几个常用的静态方法:
– Promise.all([p1, p2]):并行执行多个异步操作,全部成功后返回结果数组;只要有一个失败,就触发reject。
比如同时请求用户信息和商品列表:
const fetchUser = fetch('/api/user');
const fetchProducts = fetch('/api/products');
Promise.all([fetchUser, fetchProducts])
.then(responses => Promise.all(responses.map(res => res.json())))
.then(([user, products]) => {
console.log('用户:', user);
console.log('商品:', products);
})
.catch(err => console.error('请求失败:', err));
– Promise.race([p1, p2]):多个异步操作竞赛,哪个先完成就用哪个的结果(不管成功或失败)。
常用场景是超时处理:如果请求超过5秒就报错:
function timeout(ms) {
return new Promise((_, reject) => setTimeout(() => reject(new Error('请求超时')), ms));
}
// 要么1秒内拿到用户信息,要么超时
Promise.race([getUser(), timeout(1000)])
.then(user => console.log('用户:', user))
.catch(err => console.error(err)); // 超过1秒输出“请求超时”
三、Async/Await:让异步代码像同步一样好写
Async/Await是ES8引入的语法糖,本质是基于Promise的——它把Promise的链式调用转换成“同步”写法,代码更扁平、更好读。
1. 基本语法
async
:声明一个异步函数,函数返回的是Promise对象;await
:等待一个Promise解决(fulfilled)或拒绝(rejected),只能在async
函数里使用。
用Async/Await改写之前的例子:
async function getUserOrders() {
try {
const user = await getUser(); // 等待getUser()完成,拿到用户信息
const orders = await getOrders(user.id); // 再等待订单请求完成
console.log('用户订单:', orders); // 2秒后输出
} catch (error) {
console.error('出错了:', error); // 统一处理错误
}
}
getUserOrders();
是不是和同步代码几乎一样?try/catch
代替了catch()
,错误处理更直观。
2. 常见技巧与误区
- 并行执行多个异步操作:如果两个异步操作没有依赖关系,不要用
await
顺序执行——会浪费时间。比如同时请求用户和商品:async function fetchData() { // 错误写法:顺序执行,总时间=1秒+1秒=2秒 // const user = await fetchUser(); // const products = await fetchProducts(); // 正确写法:并行执行,总时间=1秒(取最长的那个) const [user, products] = await Promise.all([fetchUser(), fetchProducts()]); console.log('用户:', user); console.log('商品:', products); }
- await的错误处理:如果
await
的Promise被reject,会抛出错误,必须用try/catch
捕获,否则会导致未捕获的错误。比如:async function fetchData() { // 没写try/catch,reject会导致程序崩溃 const user = await fetchUser(); // 如果fetchUser()失败,这里会抛出错误 }
- async函数的返回值:async函数不管return什么,都会被包装成Promise。比如:
async function getNumber() { return 123; // 等价于return Promise.resolve(123) } getNumber().then(num => console.log(num)); // 输出123
四、实战:用Promise+Async/Await封装请求
实际项目中,我们常常用fetch
或axios
请求接口,用Promise封装后更易用:
// 封装一个带错误处理的fetch函数
async function request(url, options = {}) {
try {
const response = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
...options
});
if (!response.ok) { // 判断HTTP状态码(比如404、500)
throw new Error(`请求失败:${response.statusText}`);
}
return await response.json(); // 返回JSON数据
} catch (error) {
console.error('请求错误:', error);
throw error; // 重新抛出错误,让调用者处理
}
}
// 使用封装后的函数
async function getAppData() {
try {
const [user, products] = await Promise.all([
request('/api/user'),
request('/api/products')
]);
return { user, products };
} catch (error) {
console.error('获取数据失败:', error);
// 这里可以做错误提示,比如弹 Toast
}
}
getAppData().then(data => console.log('应用数据:', data));
这个封装的好处是:统一处理了HTTP错误(比如404)、JSON解析、错误日志,调用者只需要关注业务逻辑。
五、最佳实践总结
- 优先用Async/Await:比Promise链更易读,尤其是处理多个异步操作时;
- 永远处理错误:用
catch()
(Promise)或try/catch
(Async/Await)捕获所有可能的错误; - 并行执行无依赖的异步操作:用
Promise.all
代替顺序await
,提升性能; - 避免嵌套的Promise:如果Promise链超过3层,换成Async/Await会更清晰;
- 用Promise封装旧的回调函数:比如Node.js的
fs.readFile
,可以用util.promisify
转换成Promise版本:const fs = require('fs'); const util = require('util'); const readFile = util.promisify(fs.readFile); // 转换成Promise版 async function readConfig() { const data = await readFile('./config.json', 'utf8'); return JSON.parse(data); }
最后想对你说:异步编程的关键不是“记住API”,而是“理解状态变化”——不管用Promise还是Async/Await,本质都是管理异步操作的“结果什么时候回来”。多写几个例子,比如封装接口、处理并行请求,你会发现它其实没那么难。
原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/178