JavaScript 中 async/await 的有趣解释
JavaScript 中,有三种方式编写异步任务。
第一种方法是使用回调。当一个异步操作完成后,执行一个回调函数(意思是当操作完成后再回调我):
const callbackFunction = (result = {
// Called when the operation completes
})
asyncOperation(params, callbackFunction)
但只要处理多个异步操作,回调函数就会互相嵌套,最后陷入回调地狱。
promise 是一个异步任务结果的占位对象。使用 promise,你可以更容易地处理异步操作。
const promise = asyncOperation(params)
promise.then((result) => {
// Called when the operation completes
})
有没有看到 .then().then()...then()
promise 链 🚂🚃🚃🚃🚃?promise 问题是它们很冗长。
最终,async/await
语法被设计出来(ES2017)。它可以让你简单地以同步方式编写异步代码:
;(async function () {
const result = await asyncOperation(params)
// Called when the operation completes
})()
在这篇文章中,我将逐步解释如何在 JavaScript 中使用 async/await
。
注意:async/await
是在 Promise
之上的语法糖,建议在继续之前先熟悉一下 Promise。
同步扩展
因为帖子的标题提到了一个有趣的解释,所以我将就一个贪婪的老板的故事逐步解释 async/await
。
让我们从一个简单的(同步)函数开始,它的任务是计算加薪:
function increaseSalary(base, increase) {
const newSalary = base + increase
console.log(`New salary: ${newSalary}`)
return newSalary
}
increaseSalary(1000, 200) // => 1200
// logs "New salary: 1200"
increaseSalary()
是一个将 2 个数相加的函数,n1 + n2
是一个同步操作。
老板不希望员工的工资快速增加 ☹。所以你不允许在 increaseSalary()
函数中使用加法运算符 +
。
相反,你必须使用一个慢速函数,需要 2 秒的时间来总结数字。让我们把这个函数命名为 slowAddition()
:
function slowAddition(n1, n2) {
return new Promise((resolve) => {
setTimeout(() => resolve(n1 + n2), 2000)
})
}
slowAddition(1, 5).then(sum => console.log(sum))
// After 2 seconds logs "6"
slowAddition()
返回一个承诺,在延迟 2 秒后解析为参数之和。如何更新 increaseSalary()
函数来支持慢加法?
异步扩展
第一个天真地尝试是用调用 slowAddition(n1,n2)
代替 n1 + n2
的表达式:
function increaseSalary(base, increase) {
const newSalary = slowAddition(base, increase)
console.log(`New salary: ${newSalary}`)
return newSalary
}
increaseSalary(1000, 100) // => [object Promise]
// Logs "New salary [object Promise]"
不幸的是,函数 increaseSalary()
不知道如何处理承诺。该函数认为 promise 是常规对象:它不知道如何以及何时从 promise 中提取值。
现在是时候让 increaseSalary()
知道如何使用 async/await
语法处理 slowAddition()
返回的 promise。
首先,你需要在函数声明附近添加 async
关键字。然后,在函数体内部,你需要使用 await
操作符来使函数等待 promise 被解析。
让我们对 increaseSalary()
函数进行这些修改:
async function increaseSalary(base, increase) {
const newSalary = await slowAddition(base, increase)
console.log(`New salary: ${newSalary}`)
return newSalary
}
increaseSalary(1000, 200) // => [object Promise]
// After 2 seconds logs "New salary 1200"
JavaScript 以如下方式评估 const newSalary = await slowAddition(base, increase)
:
await slowAddition(base, increase)
暂停了increaseSalary()
函数的执行。- 在 2 秒后,由
slowAddition(base,increase)
返回的承诺会被解析出来。 increaseSalary()
函数恢复执行。newSalary
是用 promise 的解析值1200(1000 + 200)
来分配的。- 函数执行照常进行。
简单来说,当 JavaScript 在一个异步函数中遇到 await promise
时,它会暂停函数的执行,直到 promise 被解析。promise 的解析值就成了 await promise
评估的结果。
尽管 return newSalary
返回的是数字 1200
,但如果你看看函数 increaseSalary(1000, 200)
返回的实际值—它仍然是一个 promise!
一个 async
函数总是返回一个 promise,它解析为函数体内部的返回值:
increaseSalary(1000, 200).then((salary) => {
salary // => 1200
})
async
函数返回 promise 是一件好事,因为你可以嵌套异步函数。
错误异步扩展
老板提出了慢慢加薪的要求,这是不公平的。你决定破坏慢速加法 slowAddition()
函数。
你修改慢加法函数,拒绝数字加法:
function slowAdditionBroken(n1, n2) {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Unable to sum numbers')), 3000)
})
}
slowAdditionBroken(1, 5).catch(e => console.log(e.message))
// After 3 seconds logs "Unable to sum numbers"
怎样在 calculateSalary()
异步函数中针对 rejected promise?只需要把 await
操作包含在 try/catch
块中:
async function increaseSalaryBroken(base, increase) {
let newSalary
try {
newSalary = await slowAdditionBroken(base, increase)
}
catch (e) {
console.log(`Error: ${e.message}`)
newSalary = base * 2
}
console.log(`New salary: ${newSalary}`)
return newSalary
}
increaseSalaryBroken(1000, 200)
// After 3 seconds logs
// "Error: Unable to sum numbers", "New salary: 2000"
在表达式 await slowAdditionBroken(base, increase)
处,JavaScript 暂停函数的执行,并等待 promise 的实现(promise 成功解决)或拒绝(发生错误)。
3 秒后,用 new Error('Unable to sum numbers')
拒绝 promise。因为拒绝,函数执行跳入 catch (e){ }
子句,在该子句中,基本工资乘以 2。
Miser 支付了两倍的工资。现在老板要支付双倍工资。很好!
如果你没有抓住一个被拒绝的 promise,那么错误就会传播,async
异步函数返回的 promise 就会被拒绝:
async function increaseSalaryBroken(base, increase) {
const newSalary = await slowAdditionBroken(base, increase)
return newSalary
}
increaseSalaryBroken(1000, 200).catch((e) => {
e.message // => "Unable to sum numbers"
})
嵌套异步函数
尽管在异步函数内部的 return <value>
表达式返回的是有效载荷值而不是 promise,但当异步函数被调用时,它仍然会返回一个 promise。
这是件好事,因为你可以嵌套异步函数!
例如,让我们写一个使用 slowAddition()
函数增加工资数组的异步函数:
async function increaseSalaries(baseSalaries, increase) {
const newSalaries = []
for (const baseSalary of baseSalaries) {
newSalaries.push(await increaseSalary(baseSalary, increase))
}
console.log(`New salaries: ${newSalaries}`)
return newSalaries
}
increaseSalaries([950, 800, 1000], 100)
// After 6 seconds logs "New salaries: 1050,900,1100"
await salaryIncrease(baseSalary, increase)
对数组中的每个工资都被调用 3 次。每次 JavaScript 都会等待 2 秒,直到计算出总和。
这样就可以把 async
函数嵌套到 async
函数中。
并行 async
在前面的求和工资数组的例子中,求和是按顺序进行的:每增加一个工资,函数就暂停 2 秒。
但是你可以并行地进行工资增加! 让我们使用 Promise.all()
实用函数来同时启动所有的加薪。
async function increaseSalaries(baseSalaries, increase) {
const salariesPromises = []
for (const baseSalary of baseSalaries) {
salariesPromises.push(increaseSalary(baseSalary, increase))
}
const newSalaries = await Promise.all(salariesPromises)
console.log(`New salaries: ${newSalaries}`)
return newSalaries
}
increaseSalaries([950, 800, 1000], 100)
// After 2 seconds logs "New salaries: 1050,900,1100"
加薪任务马上开始(在 increaseSalary(baseSalary, increase)
附近没有使用 await
),并在 salariesPromises
中收集 promise。
await Promise.all(salitsPromises)
则暂停函数的执行,直到所有并行处理的异步操作完成。最后,只有在 2 秒后,newSalaries
变量才会包含增加的薪水。
你成功地在短短 2 秒内增加了所有员工的工资,即使每个操作都很慢,需要 2 秒。你又骗过了老板!
async 函数例子
当你想使用 async/await
语法时,常见情况是获取远程数据。
fetch()
方法是与 async/await
一起使用的一个很好的候选方法,因为它返回一个 promise,该 promise 解析到远程 API 返回的值。
例如,以下是你如何从远程服务器获取电影列表的方法:
async function fetchMovies() {
const response = await fetch('https://api.example.com/movies')
if (!response.ok) {
throw new Error('Failed to fetch movies')
}
const movies = await response.json()
return movies
}
await fetch('https://api.example.com/movies')
要暂停执行 fetchMovies()
,直到请求完成。然后你可以使用 await response.json()
来提取实际。
总结
async/await
是在 promise 之上的语法糖,提供了一种以同步方式处理异步任务的方法。
async/await
有 4 条简单的规则:
- 一个处理异步任务的函数必须使用
async
关键字来标记。 await promise
操作符暂停函数的执行,直到 promise 被成功解析或被拒绝。- 如果 promise 解析成功,
await
操作符返回解析值:const resolvedValue = await promise
。否则,你可以在try/catch
里面捕获一个被拒绝的 promise。 - 一个异步函数总是返回一个 promise,这就提供了嵌套异步函数的能力。
本文由 吳文俊 翻译,原文地址 An Interesting Explanation of async/await in JavaScript