JavaScript异步编程
什么是异步编程?
异步编程是一种编程范式,允许程序在执行耗时操作(如网络请求、文件读写等)时,不阻塞后续代码的执行。JavaScript是单线程语言,异步编程对于提高应用性能和用户体验至关重要。
异步编程的演进
JavaScript异步编程经历了以下几个阶段:
- 回调函数:最早的异步编程方式,存在回调地狱问题
- Promise:解决了回调地狱问题,提供了更优雅的链式调用
- 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的事件循环机制:
- 当遇到
await关键字时,异步函数会暂停执行,并将后续代码放入微任务队列 - 主线程继续执行其他同步代码
- 当Promise解决后,异步函数的后续代码会被放入微任务队列
- 事件循环在执行完所有同步代码后,会依次执行微任务队列中的任务
高级用法
异步迭代器
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的最佳实践
- 始终使用try/catch处理错误:避免未捕获的Promise拒绝
- 不要过度使用await:对于不依赖的异步操作,可以并行执行
- 使用Promise.all处理并行操作:提高性能
- 避免在循环中使用await:会导致串行执行,降低性能
- 使用合适的并发控制:对于大量异步操作,限制并发数量
- 返回有意义的值:异步函数应该返回明确的结果,便于调用者处理
- 使用类型检查:在TypeScript中为异步函数添加返回类型
- 保持函数简洁:每个异步函数专注于一个任务
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等,来处理复杂的异步场景。
练习
- 创建一个异步函数,使用fetch API获取GitHub用户的仓库信息
- 实现一个并发控制函数,限制同时执行的异步操作数量
- 使用async/await和for await...of循环处理分页数据
- 比较Promise链式调用和async/await两种方式,实现相同的功能
- 实现一个带有超时处理的异步函数,使用Promise.race和setTimeout
- 创建一个异步生成器,模拟无限数据流,并使用for await...of循环消费它