11.2 异步函数

异步函数也称为“async/await”(语法关键字),是ES6期约模式在ECMAScript函数中的应用。async/await是ES8规范新增的,让以同步方式写的代码能够异步执行。

11.2.1 异步函数

ES8新增的async/await旨在解决利用异步结构组织代码的问题。为此,ECMAScript对函数进行了扩展,新增两个关键字:async和await。

1、async

async关键字用于声明异步函数,可以用在函数声明、函数表达式、箭头函数和方法上。使用async关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。如果异步函数使用return关键字返回值,这个值会被Promise.resolve()包装成一个期约。异步函数始终返回期约对象。

async function foo() {
  console.log(1);
  return 3;
}
foo().then(console.log);
console.log(2);
//返回一个原始值
async function bar() {
  console.log(4);
  return "bar";
}
bar().then(console.log);
//返回一个期约
async function baz() {
  console.log(5);
  return Promise.resolve("baz");
}
baz().then(console.log);
//抛出错误
async function qux() {
  console.log(6);
  throw new Error("qux");
}
qux().catch(console.log);
//返回一个期约,但期约被拒绝
async function quux() {
  console.log("7");
  return Promise.reject("quux");
}
quux().catch(console.log);
//1
//2
//4
//5
//6
//7
//3
//bar
//Error: qux
//baz
//quux
2、await

只能在 async 函数内部使用。它会“暂停”函数的执行,等待一个Promise对象变为解决状态,然后恢复函数的执行。它会返回Promise解决的值。如果等待的不是Promise,它会返回该值本身。它并不会阻塞整个JavaScript线程。“暂停”的只是 async 函数内部的执行。当 await 在等待时,事件循环可以继续处理其他任务(如UI事件、网络请求等)。

async function quux() {
  console.log("7");
  await Promise.resolve(3);
  console.log("8");//这行代码会等到上一行完成后才执行
}
quux();
console.log("9");
//7
//9
//8

如果抛出错误或者使用Promise.reject()则返回拒绝的期约,并不会继续运行async函数内后续的代码:

async function quux() {
  console.log("7");
  await (() => { throw 3; })();
  //await Promise.reject(3);
  console.log("8");//这行代码不会执行
}
quux().catch(console.log);
console.log("9");
//7
//9
//3

11.2.2 停止和恢复执行

async/await中真正起作用的是await。async关键字只不过是一个标识符,异步函数如果没有await关键字,其执行基本上与普通函数没什么区别。JavaScript运行时遇到await关键字时,会记录在哪里暂停执行。等到await右边的值可用了,JavaScript运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。

async function foo() {
  console.log(2);
  console.log(await Promise.resolve(6));
  console.log(7);
}
async function bar() {
  console.log(4);
  console.log(await 8);
  console.log(9);
}
console.log(1);
foo();
console.log(3);
bar();
console.log(5);
//1
//2
//3
//4
//5
//6
//7
//8
//9

11.2.3 现代开发中的最佳实践与注意事项

1. 错误处理:try...catch

使用 try...catch 块来捕获 await 表达式可能抛出的错误(即Promise被拒绝)。

async function myFunc() {
  try {
    const data = await fetch(‘/api/data’);
    const json = await data.json();
    // ... 处理 json
  } catch (error) {
    // 捕获 fetch, data.json() 或处理过程中任何错误
    console.error(‘Fetch failed:‘, error);
  }
}
2. 并行执行:Promise.all

在需要等待多个不相关的异步操作时,使用 await 逐个等待会导致不必要的延迟。

// 低效的串行方式
const user = await getUser();
const posts = await getPosts();
const comments = await getComments();

// 高效的并行方式
const [user, posts, comments] = await Promise.all([
  getUser(),
  getPosts(),
  getComments()
]);

Promise.all 接受一个Promise数组,并返回一个新的Promise,当所有输入的Promise都解决时,它才解决,结果是一个包含所有结果的数组。

3. await 只能在 async 函数顶层使用

在模块的顶层作用域,你现在可以使用顶层 await(ES2022,红宝书第4版出版时可能还未完全普及)。但在普通脚本或函数中,await 必须在 async 函数内。

// 在 ES 模块中可行
const data = await fetch(‘...’).then(r => r.json());
console.log(data);
4. for...of 与异步循环

如果你想在循环中按顺序执行异步操作,可以使用 for...of 循环配合 await

for (const url of urls) {
  const response = await fetch(url);
  console.log(await response.text());
}

注意:这会串行执行所有请求。

如果需要并行,还是应该用 Promise.all

const promises = urls.map(url => fetch(url).then(r => r.text()));
const results = await Promise.all(promises);
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐