《告别回调地狱!Async/Await 异步编程实战全解析》

15 阅读4分钟

从回调地狱到优雅异步:ES8 Async/Await 完全解析

在前端开发中,异步编程是绕不开的核心知识点。从早期的回调函数,到 ES6 的 Promise,再到 ES8 推出的 Async/Await,异步代码的写法越来越优雅、可读性也越来越高。本文将结合实战代码,带你彻底搞懂 Async/Await 的使用方式和核心原理。

一、异步编程的演进

1. 回调函数:初代异步方案

最早期的异步编程依赖回调函数,比如 Node.js 中的文件读取、AJAX 请求等:

// Node.js 读取文件(回调函数版) 
const fs = require('fs');
fs.readFile('./1.html', 'utf-8', (err, data) => { 
  if (err) { 
    console.error('读取失败:', err); 
    return; 
  } 
  console.log('文件内容:', data); 
}); 

// AJAX 请求(回调函数版)
const xhr = new XMLHttpRequest(); 
xhr.open('GET', 'https://api.github.com/users/shunwuyu/repos'); 
xhr.onreadystatechange = function() { 
   if (xhr.readyState === 4 && xhr.status === 200) { 
     console.log(JSON.parse(xhr.responseText));
     }
  }; 
  xhr.send();

问题:回调嵌套过深会形成「回调地狱」,代码可读性和可维护性极差。

2. Promise:异步编程的标准化

ES6 推出的 Promise 解决了回调地狱问题,将嵌套的回调改为链式调用:

// 将文件读取封装为 Promise 
const fs = require('fs'); 
const readFilePromise = (path) => { 
  return new Promise((resolve, reject) => { 
    fs.readFile(path, 'utf-8', (err, data) => { 
      if (err) reject(err); 
      else resolve(data);
      });
    }); 
  };
  
  // Promise 链式调用 
  readFilePromise('./1.html') 
    .then(data => { 
      console.log('文件内容:', data);
      // 继续发起网络请求 
      return fetch('https://api.github.com/users/shunwuyu/repos'); 
    })
    .then(res => res.json()) 
    .then(data => console.log('GitHub 仓库数据:', data))
    .catch(err => console.error('出错了:', err));

改进:链式调用让异步流程更清晰,但仍有优化空间(比如多个 .then 链式调用依然不够直观)。

3. Async/Await:异步代码同步化

ES8(ES2017)推出的 Async/Await 是 Promise 的语法糖,让异步代码看起来和同步代码几乎一致,可读性达到顶峰。

二、Async/Await 核心用法

1. 基础语法

  • async:修饰函数,标记这是一个异步函数,异步函数的返回值会自动包装为 Promise
  • await:只能在 async 函数内部使用,等待右侧的 Promise 完成(状态变为 resolved/rejected),并返回 Promise 的结果。

2. 实战示例 1:网络请求(Fetch API)

// Async/Await 实现 GitHub 仓库数据请求 
const fetchRepos = async () => { 
  try { 
  // 等待 fetch 请求完成,获取响应对象 
  const res = await fetch('https://api.github.com/users/shunwuyu/repos');
  // 等待响应体解析为 JSON 
  const data = await res.json(); 
  console.log('GitHub 仓库列表:', data); 
  return data; // 返回值会被包装为 Promise
  } catch (err) {
  console.error('请求失败:', err);
  }
}; 

// 调用异步函数 
fetchRepos();

3. 实战示例 2:文件读取(Node.js)

const fs = require('fs').promises; // Node.js 10+ 内置 Promise 版 fs 

// Async/Await 读取文件
const readFileAsync = async () => { 
  try {  
    const html = await fs.readFile('./1.html', 'utf-8'); 
    console.log('文件内容:', html); 
    return html; 
   } catch (err) { 
     console.error('读取文件失败:', err);
  } 
}; 
readFileAsync();

4. 错误处理

Async/Await 推荐使用 try/catch 捕获异常,替代 Promise 的 .catch()

const main = async () => {
     try {
     // 可能出错的异步操作 
     const res = await fetch('https://api.github.com/users/xxx/repos'); // 不存在的用户
     const data = await res.json(); 
     console.log(data);
    } catch (err) { 
      // 统一捕获所有异步错误
    console.error('错误信息:', err.message);
   } 
};

main();

三、Async/Await 核心特性

  1. 阻塞但不卡死线程:await 只会阻塞当前 async 函数的执行流程,不会阻塞整个 JS 主线程,其他同步代码依然可以执行。
  2. 返回值是 Promise:无论 async 函数内部返回什么,最终都会被包装为 Promise 对象:
async function test() {
  return 123; 
} 
console.log(test()); // Promise { 123 }
test().then(res => console.log(res)); // 123

3.并行执行异步操作:如果多个异步操作无依赖关系,可通过 Promise.all 并行执行,提升效率:

const parallelAsync = async () => { 
  // 同时发起两个请求,而非串行等待 
  const [repoData, fileData] = await Promise.all([                                                 fetch('https://api.github.com/users/shunwuyu/repos').then(res => res.json()),      
    fs.re  adFile('./1.html', 'utf-8')
  ]); 
  console.log('仓库数据:', repoData); 
  console.log('文件数据:', fileData); 
};  

四、对比总结

image.png

五、实战建议

  1. 优先使用 Async/Await 编写异步代码,提升可读性和可维护性;
  2. 必须使用 try/catch 捕获异步错误,避免未处理的 Promise 异常;
  3. 无依赖的异步操作使用 Promise.all 并行执行,优化性能;
  4. 浏览器端注意兼容性(IE 不支持),可通过 Babel 转译;
  5. Node.js 环境建议使用内置的 Promise 版 API(如 fs.promises),避免手动封装 Promise。  总结
  6. Async/Await 是 ES8 推出的 Promise 语法糖,核心作用是让异步代码以同步的方式书写;
  7. async 标记异步函数,返回值自动包装为 Promise;await 只能在 async 函数内使用,等待 Promise 完成并返回结果;
  8. 实际开发中优先使用 Async/Await + try/catch 处理异步逻辑,无依赖的异步操作通过 Promise.all 并行执行以提升性能。