在现代 JavaScript 开发中,处理异步操作是一个不可避免的任务。传统的回调函数和 Promise 的使用虽然可以处理异步操作,但它们有时会使代码变得复杂和难以维护。为了解决这个问题,ECMAScript 2017(ES8)引入了 async 和 await 语法,使得处理异步操作变得更加简单和直观。本文将深入讲解 async/await 的使用及其背后的机制。
一、基本概念
1.1 什么是 async 函数?
在 JavaScript 中,使用 async 关键字可以定义一个异步函数。异步函数会隐式地返回一个 Promise 对象。如果函数内部有返回值,则 Promise 会被解析为该返回值;如果抛出异常,Promise 会被拒绝为该错误。
async function example() {
return "Hello, async!";
}
example().then(result => console.log(result)); // 输出: Hello, async!
1.2 什么是 await 关键字?
await 关键字只能在 async 函数内部使用。它用于等待一个 Promise 对象的解析。使用 await 会暂停函数的执行,直到 Promise 完成并返回结果。这样可以避免使用回调和链式调用,使代码更具可读性。
async function example() {
const result = await Promise.resolve("Hello, await!");
console.log(result); // 输出: Hello, await!
}
example();
二、如何使用 async/await
接下来,我们通过一些示例来更深入地理解 async/await 的用法。
2.1 基本用法
async function fetchData() {
let response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
let data = await response.json();
console.log(data);
}
fetchData();
在这个示例中,fetch 函数返回一个 Promise。我们使用 await 来等待这个 Promise 的解析,然后再处理响应数据。
2.2 错误处理
虽然使用 async/await 使得处理异步操作更加简单,但也需要注意错误处理。可以使用 try/catch 语句来捕获异步操作中的错误。
async function fetchData() {
try {
let response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
if (!response.ok) {
throw new Error('网络错误');
}
let data = await response.json();
console.log(data);
} catch (error) {
console.error('获取数据失败:', error);
}
}
fetchData();
2.3 串行和并行执行
使用 await 时,异步操作是串行执行的。下面是一个串行的示例:
async function fetchData() {
const response1 = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const data1 = await response1.json();
const response2 = await fetch('https://jsonplaceholder.typicode.com/posts/2');
const data2 = await response2.json();
console.log(data1, data2);
}
fetchData();
如果希望并行执行多个异步操作,可以使用 Promise.all():
async function fetchData() {
const [response1, response2] = await Promise.all([
fetch('https://jsonplaceholder.typicode.com/posts/1'),
fetch('https://jsonplaceholder.typicode.com/posts/2')
]);
const data1 = await response1.json();
const data2 = await response2.json();
console.log(data1, data2);
}
fetchData();
三、回调函数
1. 作用
回调函数是最早的一种处理异步操作的方法。通过将一个函数作为参数传递给另一个函数,在异步操作完成后调用这个回调函数来处理结果。
2. 示例
function fetchData(callback) {
setTimeout(() => {
callback("数据加载完成");
}, 1000);
}
fetchData(result => {
console.log(result); // 输出: 数据加载完成
});
3. 缺点
- 回调地狱:当有多个异步操作时,使用回调会导致代码层层嵌套,阅读和维护困难。
- 错误处理:错误处理比较复杂,需要在每个回调中处理错误。
四、Promise
1. 作用
Promise 是一种用于表示异步操作最终完成(或失败)及其结果值的对象。它可以使得异步操作的结果在未来某个时间点可用,从而避免回调地狱。
2. 示例
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("数据加载完成");
}, 1000);
});
}
fetchData().then(result => {
console.log(result); // 输出: 数据加载完成
}).catch(error => {
console.error(error);
});
3. 优点
- 链式调用:可以通过
.then()和.catch()进行链式调用,代码更加清晰。 - 集中处理:可以在Promise链的末尾集中处理错误。
4. 缺点
- 当多个 Promise 串行调用时,代码仍可能较长,尽管比回调函数更清晰。
五、async/await
1. 作用
async/await 是基于 Promise 的语法糖,使得异步代码看起来像同步代码,进一步提高了可读性和维护性。
2. 示例
async function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("数据加载完成");
}, 1000);
});
}
async function main() {
try {
const result = await fetchData();
console.log(result); // 输出: 数据加载完成
} catch (error) {
console.error(error);
}
}
main();
3. 优点
- 可读性:代码结构清晰,容易理解。
- 简化错误处理:可以使用
try/catch来处理错误。 - 更自然的控制流:使得异步代码的书写更像同步代码。
4. 缺点
- 只能在
async函数内使用await。
六、async/await错误捕获方式
1. 使用 try/catch
说明:这是最常用的错误捕获方式。当在 try 块中发生错误时,代码的执行会立即跳到 catch 块,后续的代码将不会被执行。
async function fetchData() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
if (!response.ok) {
throw new Error('网络请求失败');
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('出现错误:', error);
}
console.log('这行代码依然会执行'); // 如果有错误,仍然会执行
}
fetchData();
2. 在 .then() 和 .catch() 中处理错误
说明:使用 .then() 和 .catch() 捕获错误时,抛出的错误不会影响后续代码的执行。任何在 .then() 语句块中发生的错误会被传递到随后的 .catch() 中,确保后续代码可以继续运行。
async function fetchData() {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
return response.json(); // 这个Promise会resolved,也会处理后续的.then()
}
fetchData()
.then(data => {
console.log(data);
})
.catch(error => {
console.error('出现错误:', error);
});
console.log('这行代码会继续执行'); // 这行代码会执行
3. 全局错误处理
说明:使用全局事件处理(如 window 的 unhandledrejection 或 Node.js 的 process 的 unhandledRejection)来处理未捕获的 Promise 拒绝。这也意味着如果没有在内部捕获错误,代码将继续执行。
window.addEventListener('unhandledrejection', event => {
console.error('未处理的拒绝:', event.reason);
});
async function fetchData() {
const response = await fetch('https://jsonplaceholder.typicode.com/invalid-url');
const data = await response.json(); // 这里可能会出错
}
fetchData();
console.log('这行代码会继续执行'); // 这行代码会执行
小结
try/catch:通过try捕获异常时,错误会阻止后续代码的执行,但try块之后的代码仍会继续运行。.then()和.catch():捕获错误后,Promise 的拒绝不会阻止后续的代码执行。- 全局错误处理:未捕获的错误不会中断代码执行,捕获后您仍然可以继续运行后续代码。
1、如果错误不需要中断程序执行,那么可以使用如下方式
let userInfo = await getUserInfo().catch(error => console.log(error))
if (!userInfo) return
2、如果发生错误时需要中断,并且通过控制台明确一个统一的错误
try {
let userInfo = await getUserInfo()
console.log('不会继续执行')
let menuInfo = await getPermssion(userInfo?.userId)
} catch(error) {
console.log(error)
}
3、如果发生错误时需要中断,但是不需要控制台显示统一的错误
let userInfo = await getUserInfo().catch(error => {
console.log(error)
return Promise.reject(error)
})
console.log('不会继续执行')
let menuInfo = await getPermssion(userInfo?.userId)
因此,如果希望在发生错误时阻止后续代码的执行,应使用 try/catch 而不是 .then() 和 .catch()。如果希望让代码在发生错误的情况下继续运行,则可以选择使用 .then().catch() 或全局错误处理。
七、总结
- 回调函数:最基本的异步处理方式,容易导致嵌套和维护困难。
- Promise:提供更好的异步处理机制,支持链式调用,解决了一些回调地狱的问题。
- async/await:基于
Promise的语法糖,使得异步代码的书写更为简洁和可读,是目前推荐的处理异步操作的方式。