async/await:在前端开发中的应用
目录
1. async await
async
await
总结:
2. 为什么用async / await
3. 代码示例说明
3.1 Promise.all
3.2 用 async await 改写 Promise.all
4. async/await可以解决哪些异步问题
5. 哪些会产生回调地狱的问题
6. 为了避免回调地狱的问题,可以采用以下几种方法
6.1 使用传统回调函数方式导致的回调地狱:
6.2 使用Promise链式调用避免回调地狱
1. async await
async
- 定义:
async
是一个用于声明异步函数的关键字。当一个函数被async
修饰时,它会自动返回一个 Promise 对象。如果函数内部显式地返回了一个非 Promise 值,那么这个值会被自动包装成一个已解决的 Promise 对象。 - 用法:将
async
关键字放在函数声明或函数表达式之前。 -
async function myFunction() { return 'Hello, world!'; } myFunction().then(console.log); // 输出: Hello, world!
await
- 定义:
await
是一个用于等待 Promise 解决(resolve)或拒绝(reject)的操作符。它只能在async
函数内部使用,并且会暂停async
函数的执行,直到等待的 Promise 有结果。 - 用法:将
await
关键字放在 Promise 调用或任何返回 Promise 的表达式之前。 - 返回值:如果 Promise 被解决,
await
表达式的结果就是 Promise 解决的值。如果 Promise 被拒绝,await
表达式会抛出一个异常,这个异常可以被try...catch
语句捕获。 -
async function fetchData() { try { let response = await fetch('https://api.example.com/data'); let data = await response.json(); console.log(data); } catch (error) { console.error('Error:', error); } } fetchData();
总结:
- async、await 是 ES8(ECMAScript 2017)引入的新语法,用来简化 Promise 异步操作。使用
async
和await
可以让异步代码看起来几乎和同步代码一样,从而大大简化了代码的结构和可读性。你不再需要嵌套回调函数或链接多个Promise,代码变得更加直观和线性。- async 用来声明一个 function 是异步的,await 用来等待一个异步方法执行完成。
- await 只能出现在 async 函数中。
- 下面图片:async 声明的函数,返回的结果是一个 Promise 对象。如果在函数中 return 一个直接量,async 会把这个直接量通过
Promise.resolve()
封装成 Promise 对象。
下图:如果 async 函数没有返回值
await 等待的是它右侧的一个表达式的返回值。这个表达式的计算结果是 Promise 对象或者其他值。
await
只能用于 Promise,如果你尝试在非 Promise 上使用await
,它会导致运行时错误。async/await
不会阻塞主线程,它们是基于事件循环的,允许其他代码在异步操作等待期间继续执行。- 在
async
函数内部,你可以使用多个await
表达式,它们会按顺序执行。- 错误处理通常通过
try...catch
语句来实现,以捕获await
表达式可能抛出的异常。
async/await
使得异步代码更加清晰和易于理解,它减少了回调地狱和 Promise 链的复杂性,是现代 JavaScript 开发中处理异步操作的首选方法。注:Promise.resolve(x) 等价于 new Promise(resolve => resolve(x)),用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。
2. 为什么用async / await
async
和await
是JavaScript中用于处理异步操作的现代方法,它们提供了一种更简洁、更易读的方式来写异步代码,相比于传统的回调函数(callbacks)和Promise链,具有显著的优势。以下是使用async
和await
的几个主要原因:
简洁性:
使用async
和await
可以让异步代码看起来几乎和同步代码一样,从而大大简化了代码的结构和可读性。你不再需要嵌套回调函数或链接多个Promise,代码变得更加直观和线性。错误处理:
使用try...catch
语句可以很容易地捕获await
表达式中抛出的异常,这使得错误处理更加直接和集中。相比之下,在Promise链中处理错误可能需要多个.catch()
方法,或者在回调函数中手动检查错误。调试方便:
由于async
函数在执行时会返回一个Promise,并且可以在任何await
表达式处暂停,这使得在调试器中步进代码时更容易理解异步操作的执行流程。你可以逐步执行代码,看到每个异步操作的结果,而不需要跳过复杂的回调或Promise逻辑。并发执行:
虽然await
会暂停async
函数的执行,等待Promise的解决,但你可以通过不在每个异步操作后都使用await
,而是将它们存储在变量中,然后在需要时一起等待它们解决(例如使用Promise.all()
),来实现并发执行
3. 代码示例说明
3.1 Promise.all
是 JavaScript 中用于处理多个 Promise 对象的一个方法。它接受一个包含多个 Promise 的数组作为输入,并且只有当这个数组中的所有 Promise 都成功完成时,它才会返回一个新的 Promise,该 Promise 的结果是一个包含所有原 Promise 结果的数组。如果任何一个 Promise 失败了,
Promise.all
返回的 Promise 会立即被拒绝,返回那个失败的 Promise 的原因。const promise1 = new Promise((resolve, reject) => { setTimeout(resolve, 100, 'first'); }); const promise2 = new Promise((resolve, reject) => { setTimeout(resolve, 200, 'second'); }); const promise3 = new Promise((resolve, reject) => { setTimeout(resolve, 300, 'third'); }); Promise.all([promise1, promise2, promise3]) .then((values) => { console.log(values); // ['first', 'second', 'third'] }) .catch((error) => { console.error(error); });
创建了三个 Promise,它们分别在 100 毫秒、200 毫秒和 300 毫秒后完成。我们使用
Promise.all
来等待这三个 Promise 全部完成,然后打印出它们的结果。由于所有的 Promise 都成功完成了,所以Promise.all
返回的 Promise 也成功了,并且它的结果是一个包含所有原 Promise 结果的数组。如果其中一个 Promise 失败了,比如我们修改
promise2
让它被拒绝:const promise2 = new Promise((resolve, reject) => { setTimeout(reject, 200, 'Error in second promise'); });
那么
Promise.all
返回的 Promise 会立即被拒绝,并且它的拒绝原因会是那个失败的 Promise 的原因,即'Error in second promise'
。在这个情况下,then
方法不会被调用,而是会调用catch
方法来处理错误。
3.2 用 async await 改写 Promise.all
function waitFor(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async functiom fetchAllPromise () { try { const first = await waitFor(100).then(() => 'first') const second = await waitFor(100).then(() => 'second') const third = await waitFor(100).then(() => 'third') // 这里所有的await都顺序执行了,因此会按照100ms, 200ms, 300ms的顺序等待 // 然后打印出 'first', 'second', 'third' console.log([first, second, third]); } catch { console.error('An error occurred:', error); } } fetchAllPromises();
按照上述这样写,fetchAllPromise 函数中,因为每个
await
都会顺序地等待前一个 Promise 解决后再继续执行下一个。为了并发地等待所有 Promise,我们应该先启动所有的等待任务,然后再使用await
等待它们全部完成。这可以通过将 Promise 存储在数组中,然后使用Promise.all
来实现:function waitFor(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function fetchAllPromisesConcurrently() { try { const promise1 = waitFor(100).then(() => 'first'); const promise2 = waitFor(200).then(() => 'second'); const promise3 = waitFor(300).then(() => 'third'); // 并发等待所有Promise const results = await Promise.all([promise1, promise2, promise3]); // 打印所有结果 console.log(results); // ['first', 'second', 'third'] } catch (error) { console.error('An error occurred:', error); } } fetchAllPromisesConcurrently();
在这个例子中,
fetchAllPromisesConcurrently
函数会并发地启动三个等待任务,并且使用Promise.all
来等待它们全部完成。由于Promise.all
是并发执行的,所以它会等待最长时间的那个 Promise(在这个例子中是 300 毫秒的promise3
)完成后,再一次性返回所有结果。这样,我们就可以更高效地处理多个异步操作了。
4. async/await可以解决哪些异步问题
避免阻塞线程或进程:
使用async/await
可以避免在等待异步操作完成时阻塞线程或进程,使得应用程序能够同时执行多个异步操作,提高了程序的并发性和响应性。简化代码结构:
async/await
提供了一种更直观、易于理解的代码结构,使开发者能够以顺序方式编写异步代码,而不是嵌套回调函数。这大大简化了代码结构,使代码更加简洁和易于维护1。简化错误处理和异常传播:
使用async/await
可以简化错误处理和异常传播的过程。通过结合try...catch
语句,开发者可以方便地捕获和处理异步操作中的错误,使得异常处理更加清晰和可维护1。减少回调地狱:
async/await
可以减少回调地狱的问题。在传统的异步编程中,如果多个异步操作需要依次执行,往往会形成嵌套回调函数,导致代码结构复杂且难以维护。而async/await
可以让异步代码看起来像同步代码一样,按照顺序执行多个异步操作,从而避免了回调地狱的问题。提高代码的可读性和可维护性:
async/await
支持了异步代码的可读性和可维护性。通过简化代码结构、减少回调地狱以及简化错误处理,async/await
使得异步代码更加清晰、易于理解和维护。
在使用
async/await
时也需要注意异步代码中的错误处理、死锁和资源管理等问题,以确保代码的正确性和性能。
5. 哪些会产生回调地狱的问题
多层嵌套的回调函数:
当多个异步操作需要按顺序执行时,每个操作的回调函数可能会嵌套在另一个回调函数中,形成多层嵌套的结构。随着嵌套层级的加深,代码的可读性和可维护性会急剧下降。错误处理复杂:
在回调地狱中,每个异步操作可能都会失败,而错误处理通常需要在每个回调函数中单独进行。这不仅增加了代码的复杂度,也使得错误追踪和调试变得更加困难。难以扩展和维护:
随着业务逻辑复杂度的增加,回调地狱中的代码会变得难以理解和维护。添加新功能或修改现有逻辑可能会变得非常困难,因为需要仔细处理多层嵌套的回调函数和错误处理逻辑。代码结构不清晰:
回调地狱中的代码往往呈现出金字塔形状的结构,这使得代码的结构不清晰,难以阅读和理解。开发者需要花费更多的时间和精力来理解代码的执行流程和逻辑。
6. 为了避免回调地狱的问题,可以采用以下几种方法
使用Promise(下面有示例):
Promise是一种用于处理异步操作的对象,它可以将嵌套的回调转换为链式的.then调用,从而避免回调地狱。使用async/await(下面有示例):
async/await是基于Promise的语法糖,它可以使异步代码看起来更像同步代码,进一步简化了异步编程。通过使用async/await,可以避免回调函数的嵌套,使代码更加简洁易读。模块化和分解:
将复杂的业务逻辑拆分成更小的、独立的函数或方法,可以减少回调地狱的发生。每个函数或方法只处理一个特定的异步操作,并通过返回Promise来与其他函数或方法进行交互。使用响应式编程库:
如Reactor或RxJava等响应式编程库提供了声明式的方式来处理异步流和事件,可以极大地简化复杂业务逻辑的处理,从而避免回调地狱的问题。示例:先获取用户数据后才能获取订单数据
6.1 使用传统回调函数方式导致的回调地狱:
// 假设有两个函数,getUserData和getUserOrders,它们都是异步的并且接受回调函数 function getUserData(callback) { setTimeout(() => { // 异步操作完成后,调用回调函数并传递结果 callback(null, { userId: 123, userName: 'John Doe' }); }, 1000); } function getUserOrders(userId, callback) { setTimeout(() => { callback(null, [ { orderId: 456, product: 'Laptop' }, { orderId: 789, product: 'Smartphone' } ]); }, 1000); } // 使用回调函数方式获取用户数据和订单数据 getUserData((err, userData) => { if (err) { console.error('Error getting user data:', err); return; } // 用户数据获取成功后,会进行订单数据的获取 getUserOrders(userData.userId, (err, orders) => { if (err) { console.error('Error getting user orders:', err); return; } console.log('User data:', userData); console.log('User orders:', orders); }); });
在这个例子中,有两个嵌套的回调函数。首先,我们调用
getUserData
来获取用户数据,然后在它的回调函数中调用getUserOrders
来获取用户的订单数据。这种嵌套结构就是回调地狱的简化版。使用
async/await
来重写上面的代码:// 将异步函数转换为返回Promise的函数 // await 等待的是它右侧的一个表达式的返回值。这个表达式的计算结果是 Promise 对象或者其他值。 // await 只能用于 Promise,如果你尝试在非 Promise 上使用 await,它会导致运行时错误。 function getUserDataAsync() { return new Promise((resolve) => { setTimeout(() => { resolve({ userId: 123, userName: 'John Doe' }); }, 1000); }); } function getUserOrdersAsync(userId) { return new Promise((resolve) => { setTimeout(() => { resolve([{ orderId: 456, product: 'Laptop' }, { orderId: 789, product: 'Smartphone' }]); }, 1000); }); } // 使用async/await方式获取用户数据和订单数据 async function fetchUserDataAndOrders() { try { const userData = await getUserDataAsync(); // 获取用户 const orders = await getUserOrdersAsync(userData.userId); // 获取订单 console.log('User data:', userData); console.log('User orders:', orders); } catch (err) { console.error('An error occurred:', err); } } fetchUserDataAndOrders();
6.2 使用Promise链式调用避免回调地狱
使用Promise是避免回调地狱的一种有效方法。Promise提供了一种更清晰、更线性的方式来处理异步操作,使得代码更易于阅读和维护。以下是如何使用Promise来避免回调地狱的步骤和示例:
将异步函数转换为返回Promise的函数:
如果你有一个使用回调函数的异步函数,你可以将其改写为返回一个Promise的函数。这样,你就可以使用.then()
链式调用来处理异步结果,而不是嵌套回调函数。使用
.then()
链式调用:
Promise的.then()
方法允许你在Promise解决(resolve)后执行一个回调函数,并且这个回调函数可以返回一个新的Promise,从而允许你链式调用多个异步操作。处理错误:
使用.catch()
方法来捕获Promise链中任何一步发生的错误,这样你就不需要在每个异步操作中都显式地处理错误了。// 使用Promise方式的异步函数 function getUserData() { return new Promise((resolve, reject) => { // 模拟异步操作,比如网络请求或数据库查询 setTimeout(() => { // 异步操作完成后,调用resolve传递结果 resolve({ userId: 123, userName: 'John Doe' }); }, 1000); }); } function getUserOrders(userId) { return new Promise((resolve, reject) => { // 模拟异步操作 setTimeout(() => { // 异步操作完成后,调用resolve传递结果 resolve([ { orderId: 456, product: 'Laptop' }, { orderId: 789, product: 'Smartphone' } ]); }, 1000); }); } // 使用Promise链式调用获取用户数据和订单数据 getUserData() .then(userData => { // 在获取到用户数据后,再获取用户订单数据 return getUserOrders(userData.userId); }) .then(orders => { // 成功获取到用户数据和订单数据 console.log('User data:', userData); // 注意:这里的userData是从闭包中获取的,或者你可以在上一步中返回{ userData, orders } console.log('User orders:', orders); }) .catch(err => { // 处理任何一步发生的错误 console.error('Error:', err); });
在这个例子中,
getUserData
和getUserOrders
函数都返回了一个Promise。我们使用.then()
方法来链式调用这两个异步操作,并在最后使用.catch()
方法来捕获任何可能发生的错误。这样,我们的代码就避免了回调地狱,变得更加清晰和易于维护。