什么是异步编程?
在JavaScript中,异步编程是处理可能耗时较长操作的一种方式,避免阻塞主线程。JavaScript作为单线程语言,若没有异步机制,执行长时间任务时会导致整个应用响应变慢或无响应。
Web应用中的常见异步操作包括:
- 网络请求(AJAX、Fetch API)
- API调用(例如:我的Coze API调用示例)
- 文件读写操作
- 定时器和延时执行
- 事件监听处理
- 大量数据处理
异步编程的发展历程
JavaScript异步编程方案经历了多个阶段的发展,每种方式都有其适用场景。
1. 回调函数(Callbacks)
最早的异步处理方式,通过将函数作为参数传递:
function fetchData(callback) {
setTimeout(() => {
const data = { name: "JavaScript异步编程" };
callback(null, data); // 第一个参数通常用于错误处理
}, 1000);
}
fetchData((error, data) => {
if (error) {
console.error('获取数据失败:', error);
return;
}
console.log('获取数据成功:', data.name);
});
回调函数的问题:
- 回调地狱(Callback Hell):嵌套多层回调导致代码难以阅读和维护
- 错误处理复杂:每个回调都需要单独处理错误
- 执行流程不直观
回调地狱示例:
fetchUserData(function(userData) {
fetchUserPosts(userData.id, function(posts) {
fetchPostComments(posts[0].id, function(comments) {
fetchCommentAuthor(comments[0].authorId, function(author) {
console.log(author.name);
// 更多嵌套...
});
});
});
});
2. Promise
Promise是ES6引入的异步编程解决方案,用于表示一个异步操作的最终完成(或失败)及其结果值:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve({ name: "JavaScript异步编程" });
} else {
reject(new Error("数据获取失败"));
}
}, 1000);
});
}
fetchData()
.then(data => {
console.log('获取数据成功:', data.name);
return processData(data);
})
.then(processedData => {
console.log('数据处理完成:', processedData);
})
.catch(error => {
console.error('操作失败:', error);
})
.finally(() => {
console.log('操作结束,无论成功或失败');
});
Promise的优势:
- 链式调用,避免回调地狱
- 统一的错误处理机制
- 支持并行操作(Promise.all、Promise.race等)
Promise的常用方法:
Promise.all()
Promise.all() 接收一个Promise数组作为输入,并返回一个新的Promise。当所有输入的Promise都成功解决时,返回的Promise才会成功,并得到一个包含所有结果的数组;如果任何一个输入的Promise被拒绝,返回的Promise就会立即被拒绝,拒绝原因是第一个被拒绝的Promise的拒绝原因。
// 同时请求多个API端点
const promises = [
fetch('/api/users').then(res => res.json()),
fetch('/api/products').then(res => res.json()),
fetch('/api/orders').then(res => res.json())
];
Promise.all(promises)
.then(([users, products, orders]) => {
console.log('所有数据获取成功:');
console.log('用户数量:', users.length);
console.log('产品数量:', products.length);
console.log('订单数量:', orders.length);
})
.catch(error => {
console.error('至少一个请求失败:', error);
// 注意:如果有任何一个请求失败,就会进入这里
// 且不会返回任何成功请求的结果
});
适用场景:
- 需要等待多个异步操作全部完成后再进行下一步
- 批量处理多个互不依赖的异步请求
- 页面初始化需加载多个资源
Promise.race()
Promise.race() 同样接收一个Promise数组,但返回的新Promise会”竞争”完成 - 一旦数组中的任何一个Promise完成(无论成功或失败),返回的Promise就会立即采用那个Promise的状态和结果。
// 模拟数据请求,带有不同的响应时间
function fetchFromSource1() {
return new Promise((resolve) => {
setTimeout(() => resolve('数据源1的结果'), 3000);
});
}
function fetchFromSource2() {
return new Promise((resolve) => {
setTimeout(() => resolve('数据源2的结果'), 1000); // 更快
});
}
function fetchWithTimeout(timeoutMs) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('请求超时')), timeoutMs);
});
}
// 任何一个先完成就采用其结果
Promise.race([
fetchFromSource1(),
fetchFromSource2(),
fetchWithTimeout(2000) // 2秒超时
])
.then(result => {
console.log('最快的结果是:', result); // 数据源2的结果
})
.catch(error => {
console.error('发生错误或超时:', error);
});
适用场景:
- 实现请求超时功能
- 多源数据请求,采用最快响应的源
- 失败自动重试与备用数据源
Promise.allSettled()
ES2020引入的Promise.allSettled()方法接收一个Promise数组,但与Promise.all()不同,它会等待所有Promise都完成(无论成功还是失败),然后返回一个包含每个Promise结果的对象数组,每个对象包含状态(fulfilled或rejected)和值(或错误)。
const promises = [
fetch('/api/users').then(res => res.json()),
fetch('/api/nonexistent').then(res => res.json()), // 这个会失败
fetch('/api/products').then(res => res.json())
];
Promise.allSettled(promises)
.then(results => {
console.log('所有请求已完成,结果如下:');
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`请求${index+1}成功:`, result.value);
} else {
console.log(`请求${index+1}失败:`, result.reason);
}
});
// 提取所有成功的结果
const successfulResults = results
.filter(result => result.status === 'fulfilled')
.map(result => result.value);
console.log('所有成功的结果:', successfulResults);
});
适用场景:
- 需要知道每个Promise的执行结果,而不仅仅是全部成功
- 批量操作后的报告生成
- 当部分失败不应该影响整体流程时
Promise.any()
ES2021引入的Promise.any()接收一个Promise数组,并返回一个在任意一个输入Promise成功时立即成功的新Promise,结果就是第一个成功的Promise的值。只有所有Promise都被拒绝时,返回的Promise才会被拒绝,拒绝原因是一个AggregateError,包含所有拒绝原因。
// 模拟多个API端点,有些可能会失败
function fetchFromAPI1() {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('API1失败')), 1000);
});
}
function fetchFromAPI2() {
return new Promise((resolve) => {
setTimeout(() => resolve('API2成功的数据'), 2000);
});
}
function fetchFromAPI3() {
return new Promise((resolve) => {
setTimeout(() => resolve('API3成功的数据'), 3000);
});
}
Promise.any([fetchFromAPI1(), fetchFromAPI2(), fetchFromAPI3()])
.then(firstSuccess => {
console.log('第一个成功的结果是:', firstSuccess); // API2成功的数据
})
.catch(error => {
// 只有所有Promise都失败时才会执行这里
console.error('所有请求均失败:', error);
// error是AggregateError类型,包含所有拒绝原因
error.errors.forEach((err, i) => {
console.error(`API${i+1}错误:`, err);
});
});
适用场景:
- 数据源有多个,允许容错(尝试从多个源获取相同数据,使用第一个成功的)
- 实现”至少有一个成功”的逻辑
这些Promise方法的比较
| 方法 | 成功条件 | 失败条件 | 返回值 | ES版本 |
|---|---|---|---|---|
Promise.all() | 所有Promise均成功 | 任一Promise失败 | 所有结果的数组 | ES6 |
Promise.race() | 第一个完成的Promise成功 | 第一个完成的Promise失败 | 第一个完成的Promise的值 | ES6 |
Promise.allSettled() | 总是成功 | 不会失败 | 所有Promise状态和结果的对象数组 | ES2020 |
Promise.any() | 任一Promise成功 | 所有Promise均失败 | 第一个成功的Promise的值 | ES2021 |
3. async/await
ES8(ES2017)引入的语法糖,基于Promise,让异步代码看起来更像同步代码:
async function getData() {
try {
const data = await fetchData(); // fetchData返回Promise
console.log('获取数据成功:', data.name);
const processedData = await processData(data);
console.log('数据处理完成:', processedData);
return processedData;
} catch (error) {
console.error('操作失败:', error);
} finally {
console.log('操作结束,无论成功或失败');
}
}
// 调用异步函数
getData().then(result => {
console.log('最终结果:', result);
});
async/await优势:
- 同步化的代码风格,更易读更直观
- 使用try/catch进行统一的错误处理
- 更容易调试
实际应用场景
API请求示例
使用Fetch API结合async/await:
async function fetchUserData(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP错误! 状态码: ${response.status}`);
}
const userData = await response.json();
return userData;
} catch (error) {
console.error('获取用户数据失败:', error);
throw error;
}
}
// 使用方法
try {
const user = await fetchUserData(123);
console.log(user);
} catch (error) {
showErrorToUser(error.message);
}
并行处理多个请求
async function fetchMultipleResources() {
try {
// 同时发起多个请求
const [users, products, orders] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/products').then(r => r.json()),
fetch('/api/orders').then(r => r.json())
]);
return { users, products, orders };
} catch (error) {
console.error('获取数据失败:', error);
throw error;
}
}
异步编程最佳实践
- 始终处理错误:无论使用哪种异步方式,都要妥善处理可能的错误
- 避免过度嵌套:使用Promise链或async/await而非嵌套回调
- 适当使用并行处理:对于互不依赖的多个异步操作,使用Promise.all并行处理