async/await 从入门到精通,解锁异步编程的优雅密码

async/await 是 ES2017 引入的新语法,它基于 Promise 实现,使异步代码的编写和阅读更加直观。简单来说:

  • async 函数是一种特殊的函数,它的返回值总是一个 Promise
  • await 关键字只能在 async 函数内部使用,它可以暂停函数的执行,等待一个 Promise 被解决(resolved)或拒绝(rejected),然后继续执行

为什么需要 async/await

让我们通过一个简单的例子来对比一下传统的 Promise 链式调用和 async/await 的区别。

假设我们有一个需求:从服务器获取用户信息,然后根据用户信息获取用户的订单列表,最后根据订单列表获取订单详情。

使用 Promise 链式调用的代码可能长这样:

js 复制代码
fetchUser()
  .then(user => {
    return fetchOrders(user.id);
  })
  .then(orders => {
    return fetchOrderDetails(orders[0].id);
  })
  .then(details => {
    console.log(details);
  })
  .catch(error => {
    console.error(error);
  });

而使用 async/await 的代码则是这样的:

js 复制代码
async function getUserOrderDetails() {
  try {
    const user = await fetchUser();
    const orders = await fetchOrders(user.id);
    const details = await fetchOrderDetails(orders[0].id);
    console.log(details);
  } catch (error) {
    console.error(error);
  }
}

可以看到,使用 async/await 的代码更加线性,更接近我们编写同步代码的思维方式,大大提高了代码的可读性。

async/await 的基本用法

async 函数

async 函数的定义非常简单,只需要在 function 关键字前面加上 async 即可:

js 复制代码
async function fetchData() {
}

async 函数的返回值总是一个 Promise,无论函数内部是否显式返回一个 Promise。例如:

js 复制代码
async function greet() {
  return 'Hello, world!';
}

// 等价于
function greet() {
  return Promise.resolve('Hello, world!');
}

await 关键字

await 关键字只能在 async 函数内部使用,它的作用是暂停函数的执行,等待一个 Promise 被解决。例如:

js 复制代码
async function fetchData() {
  const response = await fetch('');
  const data = await response.json();
  return data;
}

在这个例子中,await fetch ('') 会暂停函数的执行,直到 fetch 请求完成并返回响应。然后,await response.json () 会再次暂停函数的执行,直到 JSON 数据解析完成。

需要注意的是,await 关键字只能用于 Promise。如果 await 后面跟着的不是一个 Promise,JavaScript 会自动将其包装成一个 resolved 的 Promise。例如:

js 复制代码
async function test() {
  const value = await 42;
  console.log(value); // 输出42
}

这里的 42 被自动包装成了 Promise.resolve (42)。

错误处理

在 async/await 中,我们可以使用传统的 try/catch 语句来处理异步操作中可能出现的错误。例如:

js 复制代码
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Error fetching data:', error);
    throw error; 
  }
}

当 await 的 Promise 被 rejected 时,catch 块会捕获到这个错误。这使得错误处理更加直观,就像处理同步代码中的错误一样。

async/await 的高级用法

并行执行多个异步操作

在某些情况下,我们可能有多个相互独立的异步操作,它们之间没有依赖关系,可以并行执行以提高效率。这时,我们可以使用 Promise.all 结合 async/await 来实现:

js 复制代码
async function fetchMultipleData() {
  const [user, products, settings] = await Promise.all([
    fetchUser(),
    fetchProducts(),
    fetchSettings()
  ]);
  
  return {
    user,
    products,
    settings
  };
}

在这个例子中,fetchUser ()、fetchProducts () 和 fetchSettings () 会同时开始执行,Promise.all 会等待所有 Promise 都被 resolved 后,才会继续执行后续代码。这样可以显著提高程序的性能。

在循环中使用 async/await

在循环中使用 async/await 需要特别注意,不同类型的循环可能会有不同的行为。

for 循环

在 for 循环中使用 async/await 会按照顺序依次执行每个异步操作:

js 复制代码
async function processItems(items) {
  for (const item of items) {
    await processItem(item);
  }
  console.log('All items processed');
}

在这个例子中,processItem (item) 会依次执行,只有当前一个 item 处理完成后,才会处理下一个 item。

map 方法

如果使用数组的 map 方法结合 async/await,情况会有所不同:

js 复制代码
async function processItems(items) {
  const results = items.map(item => processItem(item));
  
  await Promise.all(results);
  console.log('All items processed');
}

在这个例子中,map 方法会立即为每个 item 创建一个 Promise,这些 Promise 会并行执行。然后我们使用 Promise.all 等待所有 Promise 都完成。

forEach❌

在 forEach 中使用 await 是一个常见的误区,让我们看一个例子:

js 复制代码
async function processItems(items) {
  items.forEach(async item => {
    await processItem(item);
  });
  console.log('All items processed');
}

这段代码会有什么问题呢?答案是:console.log ('All items processed') 会在所有 item 处理完成之前就执行!

这是因为 forEach内部使用普通for循环遍历数组,它每次迭代调用传入的回调函数,整个遍历过程是同步的,并不会等待异步操作,因此forEach 方法并不支持 async/await。awite确实会暂停当前回调函数的执行,但 forEach 本身不会关心这个暂停,它会立即继续执行下一次迭代,这样所有回调函数会被依次启动,形成并行执行的效果。

要解决这个问题,我们应该使用支持 async/await 的循环结构,如 如果代码需要顺序执行,必须用for...of,

js 复制代码
async function processItems(items) {
  for (const item of items) {
    await processItem(item);
  }
  console.log('All items processed');
}

如果允许并行执行可以使用 Promise.all 和 map 方法:

js 复制代码
async function processItems(items) {
  await Promise.all(items.map(async item => {
    await processItem(item);
  }));
  console.log('All items processed');
}

这两种方法的本质都是利用了迭代器,不过侧重点有所不同。

for...of 循环直接利用了迭代器的顺序性,每次只处理一个元素。当遇到 await 时,它会暂停整个循环的执行,直到 Promise 被解决,确保每个异步操作按顺序完成。

而 Promise.all 结合 map 方法则是并行启动所有异步操作,再统一等待所有结果。map 方法会遍历数组并为每个元素创建一个 Promise,这些 Promise 会并行执行。Promise.all 则负责收集所有 Promise 的结果,并在所有 Promise 都解决后才继续执行后续代码。

处理多个 Promise 的竞争

有时候,我们可能需要同时发起多个请求,但只关心第一个完成的结果。这时可以使用 Promise.race 结合 async/await 来实现:

js 复制代码
async function fetchData() {
  const fastResponse = await Promise.race([
    fetchFromCache(),
    fetchFromNetwork()
  ]);
  
  return fastResponse;
}

在这个例子中,fetchFromCache () 和 fetchFromNetwork () 会同时发起请求,Promise.race 会返回第一个完成的 Promise 的结果。

async/await 的底层原理

基于 Promise 实现

async/await 实际上是 Promise 的语法糖,它并没有引入新的语言特性,而是在 Promise 的基础上提供了更优雅的写法。

例如,下面的 async/await 代码:

js 复制代码
async function fetchData() {
  try {
    const response = await fetch('');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error(error);
  }
}

可以转换为等价的 Promise 代码:

js 复制代码
function fetchData() {
  return fetch('')
    .then(response => {
      return response.json();
    })
    .then(data => {
      return data;
    })
    .catch(error => {
      console.error(error);
    });
}

生成器 (Generator) 与自动执行器

async/await 的底层实现还涉及到生成器 (Generator) 和自动执行器的概念。

生成器是一种特殊的函数,可以暂停执行并在稍后恢复。生成器函数使用 function * 语法定义,使用 yield 关键字暂停执行。这个在上一篇文章中说过了

下面是一个简单的生成器函数示例:

js 复制代码
function* generatorFunction() {
  console.log('Step 1');
  yield 1;
  console.log('Step 2');
  yield 2;
  console.log('Step 3');
  return 3;
}

const generator = generatorFunction();
console.log(generator.next()); // 输出 { value: 1, done: false }
console.log(generator.next()); // 输出 { value: 2, done: false }
console.log(generator.next()); // 输出 { value: 3, done: true }

async/await 的底层实现本质上是一个自动执行器,它会自动执行生成器函数,并处理 yield 出来的 Promise,直到生成器函数完成。

下面是一个简化的 async/await 自动执行器实现:

js 复制代码
function run(genFn) {
  const gen = genFn();
  
  function step(key, arg) {
    let result;
    try {
      result = gen[key](arg);
    } catch (error) {
      return Promise.reject(error);
    }
    
    const { value, done } = result;
    if (done) {
      return Promise.resolve(value);
    } else {
      return Promise.resolve(value).then(
        val => step('next', val),
        err => step('throw', err)
      );
    }
  }
  
  return step('next');
}

通过这种方式,JavaScript 引擎可以将 async/await 代码转换为基于生成器和 Promise 的实现,从而实现异步代码的同步化写法。

async/await 的常见应用场景

API 请求

async/await 最常见的应用场景之一就是处理 API 请求。例如:

js 复制代码
async function fetchUserProfile(userId) {
  try {
    const userResponse = await fetch(`https://api.example.com/users/${userId}`);
    const user = await userResponse.json();
    
    const postsResponse = await fetch(`https://api.example.com/posts?userId=${userId}`);
    const posts = await postsResponse.json();
    
    return {
      user,
      posts
    };
  } catch (error) {
    console.error('Error fetching user profile:', error);
    throw error;
  }
}

文件操作

在 Node.js 环境中,async/await 可以简化文件操作的代码:

js 复制代码
const fs = require('fs').promises;

async function readAndProcessFile(filePath) {
  try {
    const data = await fs.readFile(filePath, 'utf8');
    const processedData = data.toUpperCase();
    await fs.writeFile(filePath + '.processed', processedData);
    console.log('File processed successfully');
  } catch (error) {
    console.error('Error processing file:', error);
  }
}

数据库操作

在数据库操作中,async/await 可以让代码更加清晰,不过不常用,且局限性太大,这里不举代码示例了。

总结

回顾处理回调地狱的演进历程:从最初的回调函数嵌套,到 Promise 的链式调用,再到如今 async/await 的同步化写法,每一次进步都在提升开发体验和代码质量。而理解这些技术的底层原理和适用场景,则是我们在实际开发中做出正确选择的关键。

希望本文能够帮助你更好地掌握 async/await 这一强大工具,在面对复杂异步操作时游刃有余。当然如果文章中有错误的地方,请你一定要指出来,我会好好修正的。

相关推荐
七夜zippoe3 分钟前
前端开发中的难题及解决方案
前端·问题
晓13131 小时前
JavaScript加强篇——第七章 浏览器对象与存储要点
开发语言·javascript·ecmascript
Hockor1 小时前
用 Kimi K2 写前端是一种什么体验?还支持 Claude Code 接入?
前端
杨进军1 小时前
React 实现 useMemo
前端·react.js·前端框架
海底火旺1 小时前
浏览器渲染全过程解析
前端·javascript·浏览器
你听得到111 小时前
揭秘Flutter图片编辑器核心技术:从状态驱动架构到高保真图像处理
android·前端·flutter
驴肉板烧凤梨牛肉堡1 小时前
浏览器是否支持webp图像的判断
前端
Xi-Xu1 小时前
隆重介绍 Xget for Chrome:您的终极下载加速器
前端·网络·chrome·经验分享·github
摆烂为不摆烂1 小时前
😁深入JS(九): 简单了解Fetch使用
前端
杨进军1 小时前
React 实现多个节点 diff
前端·react.js·前端框架