<script setup> 中 async/await 的深度解析在 Vue3 的 Composition API 中,<script setup> 语法糖极大简化了组件的编写方式。但许多开发者在处理异步操作时,对 async/await 的使用场景存在困惑。今天我们就来彻底搞懂这个问题。
先看一个典型错误案例:
javascript复制// ❌ 错误写法:漏写 async 但使用了 await
onMounted(() => {
const data = await fetchData() // 报错:await is only valid in async function
result.value = data
})
这种错误在 Vue3 项目中相当常见。理解 async/await 的正确使用方式,需要从 JavaScript 的异步机制和 Vue 的生命周期特点两个维度来分析。
javascript复制// 这两个函数完全等价
async function foo() { return 42 }
function foo() { return Promise.resolve(42) }
Vue 的生命周期钩子(如 onMounted)有特定的执行时机和上下文要求。当我们在这些钩子中使用异步操作时,需要考虑:
javascript复制// ✅ 正确写法
onMounted(async () => {
try {
const data = await fetch('/api/data').then(r => r.json())
list.value = data
} catch (err) {
error.value = err.message
}
})
注意:即使使用 try/catch 包裹,也必须保留 async 关键字
javascript复制const expensiveCalculation = computed(async () => {
const res = await heavyTask() // 耗时计算
return processResult(res)
})
javascript复制watch(someRef, async (newVal) => {
await doSomethingWith(newVal)
// ...后续操作
})
javascript复制// composable 函数
export function useAsyncData() {
const data = ref(null)
const load = async () => {
data.value = await fetchData()
}
return { data, load }
}
javascript复制// ✅ 无需 async
onMounted(() => {
fetchData()
.then(data => {
result.value = data
})
.catch(err => {
console.error('Fetch failed:', err)
})
})
javascript复制// ✅ 明显不需要 async
onMounted(() => {
console.log('组件已挂载')
initState()
})
javascript复制<script setup>
// ✅ Vue 3.3+ 支持 (仍需注意 SSR 兼容性)
const data = await fetchData()
// 生命周期内仍需 async
onMounted(async () => {
// ...
})
</script>
| 方式 | 代码示例 | 适用场景 |
|---|---|---|
| try/catch | async () => { try { await... } catch(e) {...} } |
需要局部错误处理 |
| .catch() | .then().catch() |
链式调用场景 |
| 全局捕获 | window.addEventListener('unhandledrejection') |
最后的兜底方案 |
javascript复制// 顺序请求 ❌
const a = await getA()
const b = await getB()
// 并行请求 ✅
const [a, b] = await Promise.all([getA(), getB()])
javascript复制const controller = new AbortController()
onMounted(async () => {
try {
const res = await fetch('/api', {
signal: controller.signal
})
// ...
} catch (err) {
if (err.name === 'AbortError') {
console.log('请求被取消')
}
}
})
onUnmounted(() => {
controller.abort()
})
服务端渲染时需要特别注意:
javascript复制// 仅在客户端执行
onMounted(async () => {
if (process.client) {
await loadClientOnlyData()
}
})
将复杂异步逻辑提取到单独函数:
javascript复制const fetchUserData = async (userId) => {
const [profile, orders] = await Promise.all([
fetchProfile(userId),
fetchOrders(userId)
])
return { profile, orders }
}
onMounted(async () => {
userData.value = await fetchUserData(props.id)
})
javascript复制const isLoading = ref(false)
const error = ref(null)
const fetchData = async () => {
isLoading.value = true
error.value = null
try {
data.value = await api.getData()
} catch (err) {
error.value = err
} finally {
isLoading.value = false
}
}
javascript复制// useAsync.js
export function useAsync(asyncFn) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const execute = async (...args) => {
loading.value = true
try {
data.value = await asyncFn(...args)
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
return { data, error, loading, execute }
}
// 组件中使用
const { data, loading, execute } = useAsync(fetchData)
onMounted(() => execute())
typescript复制// 明确返回 Promise<特定类型>
async function getUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`)
return res.json()
}
typescript复制try {
await someAsyncTask()
} catch (err: unknown) {
if (err instanceof Error) {
console.error(err.message)
}
// 其他类型错误处理
}
javascript复制<script setup>
// 直接使用顶层 await
const data = await fetchData()
// 仍然需要 async 的情况
const handleClick = async () => {
await submitForm()
}
</script>
javascript复制// Vue 2 选项式 API
export default {
async created() {
this.data = await fetchData()
}
}
// Vue 3 组合式 API
setup() {
const data = ref(null)
onMounted(async () => {
data.value = await fetchData()
})
return { data }
}
在实际项目中,我通常会建立一个异步操作的统一处理规范,包括错误处理、加载状态、取消机制等。比如使用 axios 拦截器统一处理错误,或者在 Pinia store 中封装通用的异步 action 模板。这样既能保证代码一致性,又能减少重复的错误处理逻辑。