JavaScript异步编程实战:Promise与Async/Await全解

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

JavaScript异步编程实战: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封装请求

实际项目中,我们常常用fetchaxios请求接口,用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解析、错误日志,调用者只需要关注业务逻辑。

五、最佳实践总结

  1. 优先用Async/Await:比Promise链更易读,尤其是处理多个异步操作时;
  2. 永远处理错误:用catch()(Promise)或try/catch(Async/Await)捕获所有可能的错误;
  3. 并行执行无依赖的异步操作:用Promise.all代替顺序await,提升性能;
  4. 避免嵌套的Promise:如果Promise链超过3层,换成Async/Await会更清晰;
  5. 用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

(0)