网站

async function - JavaScript | MDN

await - JavaScript | MDN

异步函数

引入

Promise 用链式调用解决了回调地狱,但 .then() 写多了代码还是会比较长。

ES2017 引入了 async/await,它本质上是 Promise 的语法糖,但能让异步代码看起来像同步代码一样直观。

正文

定义

定义

async 用来声明一个异步函数,这个函数总是返回一个 Promise。await 用在 async 函数内部,用来等待一个 Promise 的结果。

async

async 放在 function 前面,表示这是一个异步函数。

async function sayHello() {
  return "你好";
}
 
sayHello().then(function (result) {
  console.log(result);
});

输出:你好

async 函数会自动把返回值包装成 Promise。

上面这段代码等价于:

function sayHello() {
  return Promise.resolve("你好");
}

async 的不同写法

函数声明:

async function getData() {
  return "数据";
}

函数表达式:

const getData = async function () {
  return "数据";
};

箭头函数:

const getData = async () => {
  return "数据";
};

对象方法:

const obj = {
  async getData() {
    return "数据";
  }
};

await

await 用来等待一个 Promise 的结果。

await 只能在 async 函数内部使用。

async function loadData() {
  const result = await fetch("/api/data");
  const data = await result.json();
  console.log(data);
}

执行流程:

  1. await fetch(…) → 等待请求完成
  2. 拿到 response 对象
  3. await result.json() → 等待解析完成
  4. 拿到 data

await 的本质

await 就是 .then() 的简写。

Promise 写法:

function loadData() {
  return fetch("/api/data")
    .then(function (response) {
      return response.json();
    })
    .then(function (data) {
      console.log(data);
    });
}

async/await 写法:

async function loadData() {
  const response = await fetch("/api/data");
  const data = await response.json();
  console.log(data);
}

两种写法结果完全一样,但 async/await 写起来更像同步代码。

对比 Promise 链和 async/await

Promise 链:

function getUserOrders() {
  return getUser()
    .then(function (user) {
      return getOrders(user);
    })
    .then(function (orders) {
      return getOrderDetail(orders[0]);
    })
    .then(function (detail) {
      console.log(detail);
    })
    .catch(function (error) {
      console.log(error.message);
    });
}

async/await:

async function getUserOrders() {
  try {
    const user = await getUser();
    const orders = await getOrders(user);
    const detail = await getOrderDetail(orders[0]);
    console.log(detail);
  } catch (error) {
    console.log(error.message);
  }
}

async/await 的优势

  • 代码从上往下读,逻辑清楚
  • 每一步的结果保存在变量里,方便使用
  • 不需要 .then() 嵌套

错误处理:try/catch

async/await 中用 try/catch 来处理错误。

async function loadData() {
  try {
    const response = await fetch("/api/data");
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.log("加载失败:" + error.message);
  }
}

如果 await 的 Promise 被 reject,错误会被 catch 捕获。

也可以配合 finally 使用:

async function loadData() {
  try {
    const response = await fetch("/api/data");
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.log("加载失败");
  } finally {
    console.log("请求结束");
  }
}

多个 await 的错误分别处理

async function loadData() {
  let user = null;
  let orders = null;
 
  try {
    user = await getUser();
  } catch (error) {
    console.log("获取用户失败");
    return;
  }
 
  try {
    orders = await getOrders(user);
  } catch (error) {
    console.log("获取订单失败");
  }
}

每个 await 用单独的 try/catch,可以针对不同错误做不同处理。

顺序执行和并行执行

顺序执行(一个接一个)

async function loadAll() {
  const data1 = await fetchData("/api/a");
  const data2 = await fetchData("/api/b");
  const data3 = await fetchData("/api/c");
 
  console.log(data1, data2, data3);
}

三个请求依次执行,每个等前一个完成才开始。总耗时 = 三个请求时间之和。

并行执行(同时进行)

async function loadAll() {
  const results = await Promise.all([
    fetchData("/api/a"),
    fetchData("/api/b"),
    fetchData("/api/c")
  ]);
 
  console.log(results[0], results[1], results[2]);
}

三个请求同时发出,等全部完成后拿到结果。总耗时 = 最慢的那个请求时间。

并行执行的另一种写法

async function loadAll() {
  const p1 = fetchData("/api/a");
  const p2 = fetchData("/api/b");
  const p3 = fetchData("/api/c");
 
  const data1 = await p1;
  const data2 = await p2;
  const data3 = await p3;
 
  console.log(data1, data2, data3);
}

先同时发起请求,再分别 await。效果和 Promise.all() 类似。

对比顺序和并行

方式写法耗时
顺序执行依次 await所有请求时间之和
并行执行Promise.all()最慢的请求时间

如果请求之间没有依赖关系,应该用并行执行。

条件使用 await

async function loadData(useCache) {
  let data;
 
  if (useCache) {
    data = getFromCache();
  } else {
    data = await fetchData("/api/data");
  }
 
  console.log(data);
}

await 可以放在 if 语句中,只在需要时等待。

在循环中使用 await

async function loadUsers(ids) {
  const users = [];
 
  for (const id of ids) {
    const user = await getUser(id);
    users.push(user);
  }
 
  console.log(users);
}

注意 for…of 可以配合 await,但 forEach 不行。

错误写法:

async function loadUsers(ids) {
  ids.forEach(async function (id) {
    const user = await getUser(id);
    console.log(user);
  });
}

forEach 不会等待 await,所有请求会同时发出,结果不好控制。

常见写法

写法作用
async function fn() {}声明异步函数
await promise等待 Promise 结果
try { await fn() }成功处理
catch (error) {}失败处理
Promise.all([…])并行执行多个 Promise

特点

  • async 函数总是返回 Promise
  • await 暂停执行,等 Promise 完成后继续
  • await 只能在 async 函数内部使用
  • 代码从上往下读,像同步代码一样清楚
  • try/catch 统一处理错误
  • 可以和 Promise.all() 配合实现并行请求

注意事项

  • await 不能写在普通函数里,只能写在 async 函数或顶层模块中
  • await 会让函数暂停,但不会阻塞函数外面的代码
  • 多个没有依赖的请求应该用 Promise.all() 并行执行
  • forEach 中不能使用 await,要用 for…of
  • 不要忘记 try/catch,否则错误会变成一个未捕获的 Promise rejection

理解

可以把 async/await 理解为”让异步代码排队等结果”的写法。

async 就是告诉 JS:“这个函数里有需要等的东西。”

await 就是告诉 JS:“在这里等一下,拿到结果再往下走。”

跟同步代码的区别是:同步是让整条线程停下来等,await 只是让这个函数暂停,其他代码照常运行。

就像在餐厅等菜:你点了菜(await),服务员去做别的桌的事(其他代码继续运行),你的菜好了再端给你(Promise resolve),你继续吃(函数继续执行)。

try/catch 就是提前想好:“万一菜没做出来怎么办。“

引出

async/await 是目前 JS 异步编程最推荐的写法,它建立在 JS Promise 之上。

实际开发中,async/await 经常和 JS 网络请求 配合使用,比如 fetch() 请求数据。

如果想了解异步代码底层是怎么调度的,可以继续学习 JS 事件循环JS 执行机制