引言:一个看似简单却暗藏危机的代码片段
最近在Code Review时,我看到了这样一段Vue3代码:
vue
<script setup>
import { ref } from 'vue'
const isAdmin = ref(false)
fetchUser().then(res => {
isAdmin.value = res.isAdmin
})
</script>
<template>
<button v-if="isAdmin">管理后台</button>
</template>
表面上看,这段代码逻辑清晰:获取用户权限后显示管理按钮。但实际上,它隐藏着三个致命问题,可能引发内存泄漏、状态错乱甚至SSR兼容性问题。
问题一:内存泄漏风险
为什么会泄漏?
当组件卸载时,如果异步请求还未完成,回调函数仍会尝试修改已销毁组件的状态。Vue会给出警告:"Cannot set property 'isAdmin' of null"。
解决方案:生命周期管理
vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const isAdmin = ref(false)
let abortController = null
onMounted(async () => {
abortController = new AbortController()
try {
const res = await fetchUser({ signal: abortController.signal })
isAdmin.value = res.isAdmin
} catch (e) {
if (e.name !== 'AbortError') console.error(e)
}
})
onUnmounted(() => {
abortController?.abort()
})
</script>
问题二:竞态条件(Race Condition)
什么是竞态条件?
当组件快速多次挂载/卸载(如路由跳转)时,先发起的请求可能后返回,导致旧数据覆盖新数据。
解决方案:请求标识符
javascript
let fetchId = 0
onMounted(async () => {
const currentFetch = ++fetchId
const res = await fetchUser()
if (currentFetch === fetchId) {
isAdmin.value = res.isAdmin
}
})
问题三:SSR兼容性问题
为什么SSR会出问题?
服务端渲染时,异步数据获取方式不规范会导致:
- 客户端注水(hydration)不匹配
- 页面闪烁
解决方案:使用Suspense
vue
// Parent.vue
<Suspense>
<AdminButton />
</Suspense>
// AdminButton.vue
<script setup>
const { data } = await useAsyncData('user', fetchUser)
const isAdmin = computed(() => data.value?.isAdmin || false)
</script>
企业级最佳实践
1. 使用状态管理(Pinia)
javascript
// stores/user.js
export const useUserStore = defineStore('user', {
state: () => ({ isAdmin: false }),
actions: {
async fetchUser() {
const res = await fetchUser()
this.isAdmin = res.isAdmin
}
}
})
2. 组合式函数封装
javascript
// composables/useUser.js
export function useUser() {
const isAdmin = ref(false)
const loading = ref(false)
const error = ref(null)
const fetchUser = async () => {
try {
loading.value = true
const res = await fetchUser()
isAdmin.value = res.isAdmin
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
return { isAdmin, loading, error, fetchUser }
}
3. 错误处理与用户体验
vue
<template>
<div v-if="loading">加载中...</div>
<div v-else-if="error">加载失败: {{ error.message }}</div>
<button v-else-if="isAdmin">管理后台</button>
</template>
总结:Vue3异步数据获取的黄金法则
- 始终管理生命周期:使用onUnmounted清理异步操作
- 防御竞态条件:通过标识符或AbortController控制
- 考虑SSR兼容性:优先使用Suspense或useAsyncData
- 状态管理解耦:复杂场景使用Pinia
- 完善用户体验:处理加载中、错误和空状态
讨论
在你的项目中,是如何处理异步数据加载的?有没有遇到过特别棘手的情况?欢迎在评论区分享你的经验!