JavaScript async/await

78 阅读5分钟

一、async/await 的出现背景

1. 异步编程的演进历程

  1. 回调函数时代

    getUser(id, function(user) {
      getPosts(user, function(posts) {
        console.log(posts);
      });
    });
    
    • 问题:回调地狱,错误处理困难
  2. Promise 时代

    getUser(id)
      .then(user => getPosts(user))
      .then(posts => console.log(posts))
      .catch(err => console.error(err));
    
    • 改进:链式调用,统一错误处理
    • 局限:仍然需要回调,复杂逻辑不够直观
  3. Generator + Promise

    function* gen() {
      const user = yield getUser(id);
      const posts = yield getPosts(user);
      console.log(posts);
    }
    
    • 问题:需要外部执行器,不够直接

2. async/await 的解决方案

async/await 是建立在 Promise 基础上的语法糖,它:

  • 让异步代码看起来像同步代码
  • 保留 Promise 的所有优点
  • 解决回调地狱问题
  • 提供更直观的错误处理

二、基本语法与工作原理

1. async 函数

async 关键字用于声明一个异步函数:

async function fetchData() {
  return 'data'; // 等价于 return Promise.resolve('data')
}

// 调用
fetchData().then(data => console.log(data)); // 'data'

特性

  • 总是返回 Promise
  • 返回值会被自动包装为 Promise
  • 可以包含 await 表达式

2. await 表达式

await 关键字用于等待 Promise 的解决:

async function getUserPosts(userId) {
  const user = await getUser(userId); // 等待Promise解决
  const posts = await getPosts(user.id); // 使用上一步结果
  return posts;
}

规则

  • 只能在 async 函数中使用
  • 暂停 async 函数的执行,直到 Promise 完成
  • 返回 Promise 解决的值
  • 如果 Promise 被拒绝,抛出拒绝原因(可用 try-catch 捕获)

三、错误处理机制

1. try-catch 方式

async function fetchWithRetry() {
  try {
    const data = await fetchData();
    return data;
  } catch (error) {
    console.error('Fetch failed:', error);
    // 重试逻辑
    return fetchWithRetry();
  }
}

2. 结合 Promise.catch()

async function fetchData() {
  const response = await fetch('api/data').catch(err => {
    console.error('Network error:', err);
    throw err; // 继续传递错误
  });
  return response.json();
}

3. 错误处理模式比较

方式优点缺点
try-catch同步代码风格,集中处理可能嵌套层级深
.catch()链式调用,灵活破坏同步代码风格
混合使用根据场景选择最佳方式需要风格统一

四、执行流程与控制

1. 顺序执行

async function sequential() {
  const a = await task1(); // 等待完成
  const b = await task2(); // 然后执行
  return a + b;
}

2. 并行执行

async function parallel() {
  const [a, b] = await Promise.all([task1(), task2()]);
  return a + b;
}

3. 循环中的 await

async function processArray(array) {
  for (const item of array) {
    await processItem(item); // 顺序处理
  }
  
  // 或者并行处理
  await Promise.all(array.map(item => processItem(item)));
}

五、高级用法与模式

1. async 立即执行函数

(async () => {
  const data = await fetchData();
  console.log(data);
})();

2. 类中的 async 方法

class ApiClient {
  async getData() {
    const response = await fetch('/api/data');
    return response.json();
  }
}

3. await 与 thenable 对象

// 任何实现了 then 方法的对象都可以被 await
const thenable = {
  then(resolve) {
    setTimeout(() => resolve('done'), 1000);
  }
};

async function test() {
  const result = await thenable;
  console.log(result); // 1秒后输出 'done'
}

4. 顶层 await (ES2022)

// 模块中直接使用
const data = await fetchData();
console.log(data);

// 注意:只能在模块中使用,不能在普通脚本中

六、常见陷阱与最佳实践

1. 常见错误

  1. 忘记 await

    async function example() {
      const promise = fetchData(); // 忘记await
      console.log(promise); // 输出Promise对象而非结果
    }
    
  2. 过度顺序化

    // 低效写法
    async function slow() {
      const a = await task1(); // 等待
      const b = await task2(); // 再等待
      return a + b;
    }
    
    // 高效写法
    async function fast() {
      const [a, b] = await Promise.all([task1(), task2()]);
      return a + b;
    }
    
  3. 错误处理遗漏

    // 错误可能被忽略
    async function risky() {
      const data = await fetchData(); // 如果拒绝,错误会向上抛出
      // 如果没有调用方处理,会导致未处理的Promise拒绝
    }
    

2. 最佳实践

  1. 明确错误处理

    // 方式1:try-catch
    async function safe1() {
      try {
        return await fetchData();
      } catch (err) {
        console.error(err);
        return fallbackData;
      }
    }
    
    // 方式2:.catch()
    async function safe2() {
      return await fetchData().catch(err => {
        console.error(err);
        return fallbackData;
      });
    }
    
  2. 合理并行化

    async function optimal() {
      // 无依赖的任务并行执行
      const [user, product] = await Promise.all([
        fetchUser(),
        fetchProduct()
      ]);
      
      // 有依赖的任务顺序执行
      const order = await createOrder(user, product);
      return order;
    }
    
  3. 性能优化

    // 提前启动Promise
    async function optimized() {
      const userPromise = fetchUser(); // 立即启动,不等待
      const productPromise = fetchProduct();
      
      // 需要时await
      const user = await userPromise;
      const product = await productPromise;
      
      return { user, product };
    }
    

七、与生成器函数的比较

1. 实现原理相似性

async/await 本质上是 Generator + Promise 的语法糖:

// async/await 版本
async function example() {
  const a = await task1();
  const b = await task2();
  return a + b;
}

// 生成器等效实现
function* generatorExample() {
  const a = yield task1();
  const b = yield task2();
  return a + b;
}

// 需要外部执行器来运行生成器

2. 主要区别

特性async/await生成器
返回值总是Promise任意值
执行控制自动执行需要外部执行器
目的专门用于异步通用迭代控制
语法更简洁更灵活

八、浏览器兼容性与转译

1. 兼容性情况

  • 现代浏览器全面支持
  • Node.js 7.6+ 原生支持
  • 旧环境需要转译

2. Babel 转译

async/await 通过 Babel 可以转换为 ES5 代码:

// 转换前
async function fetchData() {
  const res = await fetch('/data');
  return res.json();
}

// 转换后(简化版)
function fetchData() {
  return _asyncToGenerator(function* () {
    const res = yield fetch('/data');
    return res.json();
  })();
}

3. 性能考虑

  • 原生 async/await 性能接近原生 Promise
  • 转译后的代码会有额外开销
  • 在关键性能路径谨慎使用

九、实际应用场景

1. API 请求处理

async function fetchUserWithPosts(userId) {
  try {
    const user = await fetch(`/users/${userId}`);
    const posts = await fetch(`/users/${userId}/posts`);
    return { user, posts };
  } catch (error) {
    console.error('Failed to fetch data:', error);
    throw error;
  }
}

2. 文件操作(Node.js)

const fs = require('fs').promises;

async function processFiles() {
  const content1 = await fs.readFile('file1.txt', 'utf8');
  const content2 = await fs.readFile('file2.txt', 'utf8');
  await fs.writeFile('combined.txt', content1 + content2);
}

3. 数据库操作

async function createUserWithProfile(userData, profileData) {
  const connection = await db.getConnection();
  try {
    await connection.beginTransaction();
    const user = await connection.query('INSERT INTO users SET ?', userData);
    const profile = await connection.query(
      'INSERT INTO profiles SET ?', 
      { ...profileData, userId: user.insertId }
    );
    await connection.commit();
    return { user, profile };
  } catch (error) {
    await connection.rollback();
    throw error;
  } finally {
    connection.release();
  }
}

4. 异步初始化

class App {
  static async init() {
    this.cache = await loadCache();
    this.db = await connectDatabase();
    this.server = await startServer();
  }
}

// 使用
App.init().then(() => {
  console.log('Application started');
});

十、总结

async/await 是现代 JavaScript 异步编程的核心特性:

  1. 核心优势

    • 同步风格的异步代码
    • 更清晰的错误处理
    • 更好的可读性和可维护性
  2. 关键要点

    • async 函数总是返回 Promise
    • await 暂停执行直到 Promise 完成
    • 使用 try-catch 处理错误
    • 合理组合顺序和并行执行
  3. 适用场景

    • 任何基于 Promise 的异步操作
    • 需要清晰流程控制的异步逻辑
    • 复杂的异步数据流处理
  4. 注意事项

    • 避免过度顺序化影响性能
    • 不要忘记错误处理
    • 理解底层 Promise 机制