关于异步函数
合成糖
他们总是信守承诺
它将抛出的错误转化为被拒绝的承诺。
他们总是会带来新的承诺
处理错误
总之
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
异步函数非常棒,尤其是在需要连续调用多个返回 Promise 的函数时。使用 async/await,代码更容易被人理解,因为数据流模拟了同步代码,而这正是我们习惯阅读的同步代码。
那么,异步函数究竟是什么呢?
合成糖
我第一次接触异步函数时,这句话就深深地印在了我的脑海里:
Async/await 只是 Promise 链式调用的一种语法糖。
——某人,某个时候
这基本正确,如果你对异步函数的理解是这样,那它对你来说就足够了。为了重申这一点,我们来看一个例子,并将其从 Promise 链重构为异步函数:
function fetchTodos() {
return fetch('/todos')
.then(response => response.json())
.then(json => json.data)
}
目前为止一切顺利。这里没什么太难的,只是常规的数据获取和提取(这里故意省略了错误处理)。不过,即使是这个例子,回调函数也不太容易阅读,那么如果用异步函数会怎么样呢?
async function fetchTodos() {
const response = await fetch('/todos')
const json = await response.json()
return json.data
}
啊,我觉得这样读起来好多了,因为你可以清楚地看到我们把变量赋值给了哪里,以及该函数的最终返回值是什么。
所以,如果这确实是一个不错的异步函数思维模型,那么上述定义的问题是什么呢?嗯,它并不全面。Promise 链和异步函数之间存在一些细微的差别,而这些差别是我在实践中摸索出来的。让我们逐一了解一下:
他们总是信守承诺
这实际上是异步函数的本质特征。无论你做什么,它总是会返回一个 Promise,即使你没有显式地返回一个 Promise:
async function fetchRandom() {
// ✅ this will return `Promise<number>`
return Math.random()
}
这是必要的,因为你可以在异步函数中使用`await`关键字,一旦使用了 `await`,你就进入了 Promise 的世界,而 Promise 的世界是无法回退的。如果代码是异步的,你就无法将其转回同步代码。我个人对此感到非常困惑,因为在 Scala 中,`await.result`实际上接收一个 Promise 对象,阻塞执行一段时间,然后允许你使用解析后的值继续同步执行。
然而,在 JavaScript 中,异步函数会始终保持异步,因此其返回值必须是 Promise,而语言本身也确保了这一点。这就引出了下一个要点:
它将抛出的错误转化为被拒绝的承诺。
你可能在涉及fetch API 的示例代码中见过这种情况,因为 fetch 不会像其他库(例如axios )那样,在出现错误状态码时自动返回一个失败的 Promise 。要获得一个失败的 Promise,你只需抛出一个 Error(或者任何其他类型的异常),它就会被转换为一个失败的 Promise。这是因为,异步函数总是需要返回一个 Promise:
async function fetchTodos() {
const response = await fetch('/todos')
if (!response.ok) {
// ✅ this will become a failed promise
throw new Error('Network response was not ok')
}
return response.json()
}
如果你在 Promise 链中,这种方法有效;但如果你在Promise 链之外,则无效。假设你想进行一些参数验证,并决定在一个非异步函数中,如果输入无效则抛出一个 Error 异常:
function fetchTodo(id: number | undefined) {
if (!id) {
// 🚨 this will NOT give you a failed promise
throw new Error("expected id")
}
return fetch('/todos')
.then(response => response.json())
.then(json => json.data)
}
如果将同一个函数设为异步函数,就会返回一个失败的 Promise。这些细微差别很容易让人困惑,所以我无论在什么情况下都更喜欢显式地使用Promise.reject :
function fetchTodo(id: number | undefined) {
if (!id) {
// ✅ this will work as expected, no matter where
return Promise.reject(new Error("expected id"))
}
return fetch('/todos')
.then(response => response.json())
.then(json => json.data)
}
他们总是会带来新的承诺
我第一次遇到这个问题是在使用react-query 处理查询取消的时候。react-query 要求我们在返回的 Promise 对象上附加一个.cancel方法。令人惊讶的是,这在异步函数中并不完全有效:
async function fetchTodos() {
const controller = new AbortController()
const signal = controller.signal
const promise = fetch('/todos', {
signal,
})
promise.cancel = () => controller.abort()
// 🚨 This will be a new promise without the cancel method!
return promise
}
因为我们身处异步函数中,即使我们自己已经返回了一个 Promise,函数结束时仍然会返回一个新的Promise!如果你想了解如何在异步函数中取消查询,可以阅读这篇很棒的文章。
处理错误
异步函数中处理错误的默认方式是使用 try/catch,我不太喜欢这种方式,主要是因为 try/catch 的作用域似乎会变得非常大。如果异步操作之后还有同步代码执行,并且这些同步代码可能失败,我们很可能仍然会将其视为获取数据失败的情况:
const fetchTodos = async (): Promise<Todos | undefined> => {
try {
const response = await axios.get('/todos')
// 🚨 if tranform fails, we will catch it and show a toast :(
return transform(response.data)
} catch (error) {
showToast("Fetch failed: " + error.message)
return undefined
}
}
有时,我们甚至会默默地捕获并丢弃错误,这将使调试变得非常困难。
所以,如果你也觉得 async/await 很酷,但 try/catch 不怎么样,你可以尝试将异步函数与“传统”的 catch 方法结合起来:
const fetchTodos = async (): Promise<Todos | undefined> => {
const response = await axios.get('/todos').catch(error => {
// 🚀 showing the toast is scoped to catching the response error
showToast("Fetch failed: " + error.message)
return undefined
})
return transform(response?.data)
}
总之
我希望这能让你对 async/await 的底层工作原理有更深入的了解。我见过很多代码,其中async关键字被毫无理由地用在函数上,所以最后,这里列举一些我认为应该避免的模式:
// 🚨 the async keyword doesn't do anything -
// except creating a new unneccessary promise
const fetchTodos = async () => axios.get('/todos')
const fetchTodos = async () => {
const response = await axios.get('/todos')
// ⚠️ awaiting a non-promise is possible, but doesn't do anything
return await response.data
}
// 🙈 I don't even 🤷♂️
const fetchTodos = async () =>
await axios.get('/todos').then(response => response.data)
const fetchTodos = async () => {
try {
// 🚨 await is redundant here, too
return await axios.get('/todos')
} catch (error) {
// 🚨 the catch-and-throw is totally unnecessary
throw error
}
}
今天就到这里啦。如果您有任何问题,欢迎在推特上联系我
,或者在下方留言⬇️