发布于 2026-01-05 8 阅读
0

关于异步函数 语法糖 它们总是返回一个 Promise 它将抛出的错误转换为被拒绝的 Promise 它们总是返回一个新的 Promise 错误处理 总结 DEV 全球展示挑战赛,由 Mux 呈现:展示你的项目!

关于异步函数

合成糖

他们总是信守承诺

它将抛出的错误转化为被拒绝的承诺。

他们总是会带来新的承诺

处理错误

总之

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

异步函数非常棒,尤其是在需要连续调用多个返回 Promise 的函数时。使用 async/await,代码更容易被人理解,因为数据流模拟了同步代码,而这正是我们习惯阅读的同步代码。

那么,异步函数究竟是什么呢?

合成糖

我第一次接触异步函数时,这句话就深深地印在了我的脑海里:

Async/await 只是 Promise 链式调用的一种语法糖。

——某人,某个时候

这基本正确,如果你对异步函数的理解是这样,那它对你来说就足够了。为了重申这一点,我们来看一个例子,并将其从 Promise 链重构为异步函数:

function fetchTodos() {
  return fetch('/todos')
    .then(response => response.json())
    .then(json => json.data)
}
Enter fullscreen mode Exit fullscreen mode

目前为止一切顺利。这里没什么太难的,只是常规的数据获取和提取(这里故意省略了错误处理)。不过,即使是这个例子,回调函数也不太容易阅读,那么如果用异步函数会怎么样呢?

async function fetchTodos() {
  const response = await fetch('/todos')
  const json = await response.json()
  return json.data
}
Enter fullscreen mode Exit fullscreen mode

啊,我觉得这样读起来好多了,因为你可以清楚地看到我们把变量赋值给了哪里,以及该函数的最终返回值是什么。

所以,如果这确实是一个不错的异步函数思维模型,那么上述定义的问题是什么呢?嗯,它并不全面。Promise 链和异步函数之间存在一些细微的差别,而这些差别是我在实践中摸索出来的。让我们逐一了解一下:

他们总是信守承诺

这实际上是异步函数的本质特征。无论你做什么,它总是会返回一个 Promise,即使你没有显式地返回一个 Promise:

async function fetchRandom() {
  // ✅ this will return `Promise<number>`
  return Math.random()
}
Enter fullscreen mode Exit fullscreen mode

这是必要的,因为你可以在异步函数中使用`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()
}
Enter fullscreen mode Exit fullscreen mode

如果你在 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)
}
Enter fullscreen mode Exit fullscreen mode

如果将同一个函数设为异步函数,就会返回一个失败的 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)
}
Enter fullscreen mode Exit fullscreen mode

他们总是会带来新的承诺

我第一次遇到这个问题是在使用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
}
Enter fullscreen mode Exit fullscreen mode

因为我们身处异步函数中,即使我们自己已经返回了一个 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
  }
}
Enter fullscreen mode Exit fullscreen mode

有时,我们甚至会默默地捕获并丢弃错误,这将使调试变得非常困难。

所以,如果你也觉得 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)
}
Enter fullscreen mode Exit fullscreen mode

总之

我希望这能让你对 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
  }
}
Enter fullscreen mode Exit fullscreen mode

今天就到这里啦。如果您有任何问题,欢迎在推特上联系我
,或者在下方留言⬇️

文章来源:https://dev.to/tkdodo/about-async-functions-a0e