JavaScript异步编程

什么是异步编程?

异步编程是一种编程范式,允许程序在执行耗时操作(如网络请求、文件读写等)时,不阻塞后续代码的执行。JavaScript是单线程语言,异步编程对于提高应用性能和用户体验至关重要。

异步编程的演进

JavaScript异步编程经历了以下几个阶段:

  1. 回调函数:最早的异步编程方式,存在回调地狱问题
  2. Promise:解决了回调地狱问题,提供了更优雅的链式调用
  3. async/await:ES2017引入的语法糖,基于Promise,提供了更接近同步代码的异步编程体验

async/await基础

async函数

使用async关键字声明的函数称为异步函数,它有以下特点:

  • 自动返回一个Promise对象
  • 内部可以使用await关键字
// 声明异步函数
async function fetchData() {
  return 'Hello, World!';
}

// 调用异步函数
fetchData().then(result => {
  console.log(result); // Hello, World!
});

await关键字

await关键字只能在异步函数内部使用,它的作用是:

  • 暂停异步函数的执行,等待Promise解决
  • 解析Promise的结果,并继续执行异步函数
  • 如果Promise被拒绝,抛出异常,可以用try/catch捕获
// 异步函数中使用await
async function fetchData() {
  const promise = new Promise((resolve) => {
    setTimeout(() => resolve('Data fetched!'), 1000);
  });
  
  const result = await promise;
  console.log(result); // Data fetched!
  return result;
}

fetchData();

async/await与Promise的关系

async/await是基于Promise的语法糖,它们可以互相转换:

Promise链式调用

function fetchData() {
  return fetch('https://api.example.com/data')
    .then(response => response.json())
    .then(data => {
      console.log(data);
      return data;
    })
    .catch(error => {
      console.error('Error:', error);
    });
}

async/await版本

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
    return data;
  } catch (error) {
    console.error('Error:', error);
  }
}

错误处理

async/await使用try/catch来处理异步操作中的错误,比Promise的catch方法更直观:

基本错误处理

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Fetch error:', error.message);
    // 可以选择重新抛出错误
    // throw error;
    // 或者返回默认值
    return { error: error.message };
  }
}

并行错误处理

async function fetchMultipleData() {
  try {
    // 并行执行多个异步操作
    const [data1, data2, data3] = await Promise.all([
      fetch('https://api.example.com/data1').then(res => res.json()),
      fetch('https://api.example.com/data2').then(res => res.json()),
      fetch('https://api.example.com/data3').then(res => res.json())
    ]);
    
    console.log('Data1:', data1);
    console.log('Data2:', data2);
    console.log('Data3:', data3);
    
    return { data1, data2, data3 };
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

异步函数的执行流程

同步与异步代码的混合

async function mixedCode() {
  console.log('1. Start');
  
  // 异步操作,暂停执行
  await new Promise(resolve => setTimeout(resolve, 1000));
  
  console.log('2. After await');
  
  // 同步代码继续执行
  console.log('3. End');
}

mixedCode();
console.log('4. Outside function');

// 输出顺序:
// 1. Start
// 4. Outside function
// 2. After await
// 3. End

事件循环与异步函数

异步函数的执行依赖于JavaScript的事件循环机制:

  1. 当遇到await关键字时,异步函数会暂停执行,并将后续代码放入微任务队列
  2. 主线程继续执行其他同步代码
  3. 当Promise解决后,异步函数的后续代码会被放入微任务队列
  4. 事件循环在执行完所有同步代码后,会依次执行微任务队列中的任务

高级用法

异步迭代器

ES2018引入了异步迭代器,允许使用for await...of循环遍历异步数据源:

// 异步迭代器示例
async function* asyncGenerator() {
  yield await new Promise(resolve => setTimeout(() => resolve(1), 1000));
  yield await new Promise(resolve => setTimeout(() => resolve(2), 1000));
  yield await new Promise(resolve => setTimeout(() => resolve(3), 1000));
}

// 使用for await...of循环
async function iterateAsync() {
  for await (const value of asyncGenerator()) {
    console.log(value); // 1, 2, 3(每次间隔1秒)
  }
}

iterateAsync();

异步生成器

异步生成器是返回异步迭代器的函数,使用async function*声明:

// 异步生成器示例
async function* fetchPaginatedData(url) {
  let page = 1;
  let hasMore = true;
  
  while (hasMore) {
    const response = await fetch(`${url}?page=${page}`);
    const data = await response.json();
    
    yield data.items;
    
    hasMore = data.hasMore;
    page++;
  }
}

// 使用异步生成器
async function processAllData() {
  for await (const items of fetchPaginatedData('https://api.example.com/items')) {
    console.log('Processing', items.length, 'items');
    // 处理数据...
  }
}

processAllData();

并发控制

使用async/await可以方便地实现并发控制,限制同时执行的异步操作数量:

// 并发控制函数
async function concurrentControl(tasks, limit) {
  const results = [];
  const executing = [];
  
  for (const task of tasks) {
    // 创建异步任务
    const promise = Promise.resolve().then(() => task());
    results.push(promise);
    
    // 如果达到并发限制,等待一个任务完成
    if (tasks.length >= limit) {
      const execution = promise.then(() => {
        executing.splice(executing.indexOf(execution), 1);
      });
      executing.push(execution);
      
      if (executing.length >= limit) {
        await Promise.race(executing);
      }
    }
  }
  
  return Promise.all(results);
}

// 使用示例
const tasks = [
  () => fetch('https://api.example.com/data1').then(res => res.json()),
  () => fetch('https://api.example.com/data2').then(res => res.json()),
  () => fetch('https://api.example.com/data3').then(res => res.json()),
  () => fetch('https://api.example.com/data4').then(res => res.json()),
  () => fetch('https://api.example.com/data5').then(res => res.json())
];

concurrentControl(tasks, 2).then(results => {
  console.log(results);
});

async/await的最佳实践

  1. 始终使用try/catch处理错误:避免未捕获的Promise拒绝
  2. 不要过度使用await:对于不依赖的异步操作,可以并行执行
  3. 使用Promise.all处理并行操作:提高性能
  4. 避免在循环中使用await:会导致串行执行,降低性能
  5. 使用合适的并发控制:对于大量异步操作,限制并发数量
  6. 返回有意义的值:异步函数应该返回明确的结果,便于调用者处理
  7. 使用类型检查:在TypeScript中为异步函数添加返回类型
  8. 保持函数简洁:每个异步函数专注于一个任务

async/await与其他异步模式的比较

特性 回调函数 Promise async/await
代码可读性
错误处理 嵌套 链式 try/catch
调试难度
并行执行 复杂 简单(Promise.all) 简单(Promise.all + await)
学习曲线

总结

async/await是JavaScript异步编程的重大进步,它基于Promise,提供了更简洁、更易读的异步编程语法。通过async/await,可以写出更接近同步代码的异步代码,提高代码的可读性和可维护性。

async/await的主要优势包括:

  • 更简洁的语法
  • 更直观的错误处理
  • 更易调试
  • 更接近同步代码的思维模式

在实际开发中,应优先使用async/await,并结合Promise的强大功能,如Promise.all、Promise.race等,来处理复杂的异步场景。

练习

  1. 创建一个异步函数,使用fetch API获取GitHub用户的仓库信息
  2. 实现一个并发控制函数,限制同时执行的异步操作数量
  3. 使用async/await和for await...of循环处理分页数据
  4. 比较Promise链式调用和async/await两种方式,实现相同的功能
  5. 实现一个带有超时处理的异步函数,使用Promise.race和setTimeout
  6. 创建一个异步生成器,模拟无限数据流,并使用for await...of循环消费它
« 上一篇 JavaScript模块 下一篇 » JavaScript错误处理