你肯定遇到过这样的场景:调用接口获取用户信息后,还要用用户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
