Vue3 + vue-query 的重復(fù)請(qǐng)求問(wèn)題解決
前言
@tanstack/vue-query 以其強(qiáng)大的緩存和狀態(tài)管理能力,極大地簡(jiǎn)化了數(shù)據(jù)獲取邏輯。然而,當(dāng)它與 Vue3 復(fù)雜的響應(yīng)式系統(tǒng)結(jié)合時(shí),如果不深入理解其工作原理,很容易陷入“重復(fù)請(qǐng)求”的陷阱。在最近一次開(kāi)發(fā)中就遇到了這個(gè)問(wèn)題,本文展現(xiàn)了從一個(gè)頁(yè)面加載時(shí)觸發(fā)4次重復(fù)API請(qǐng)求,到2次,并最終實(shí)現(xiàn)1次請(qǐng)求的解決過(guò)程,以免下次再掉進(jìn)這個(gè)坑里。
問(wèn)題的初現(xiàn):一個(gè)“勤奮過(guò)頭”的列表頁(yè)
我們有一個(gè)需求:開(kāi)發(fā)一個(gè)包含Tabs切換、獨(dú)立篩選條件和分頁(yè)功能的數(shù)據(jù)列表頁(yè)面。技術(shù)棧是 Vue 3 (Composition API) 和 @tanstack/vue-query,并使用了一個(gè)名為 <cec-page-wrapper> 的高度封裝的表格組件。
頁(yè)面完成后,一切似乎工作正常,但打開(kāi)瀏覽器網(wǎng)絡(luò)面板,我們驚恐地發(fā)現(xiàn),每次加載頁(yè)面,獲取列表數(shù)據(jù)的API transactionList 竟然被調(diào)用了4次!
發(fā)生了什么???
第一階段:從4次請(qǐng)求到2次 —— 尋找“幽靈觸發(fā)者”
我們首先審查了代碼,試圖找出所有可能調(diào)用 loadData (即 useQuery 的 refetch) 的地方。
當(dāng)時(shí)的 script setup 核心邏輯:
// ...
const activeTab = ref('PROVIDER');
const tablePage = ref({ currentPage: 1, ... });
const filterParams = ref({ ... });
const { isFetching, data: tableData, refetch: loadData } = useQuery({
queryKey: ['transactionList', activeTab, tablePage, filterParams],
queryFn: async () => { /* ... */ },
});
watch(activeTab, () => {
tablePage.value.currentPage = 1;
loadData(); // Tab 切換時(shí)加載
});
onMounted(() => {
getBusinessNodesList(); // 獲取下拉框選項(xiàng)
loadData(); // 掛載時(shí)加載
});
// 模板中,分頁(yè)組件 @change 事件也調(diào)用了 loadData
經(jīng)過(guò)仔細(xì)的日志打印和分析,我們定位到了這4次請(qǐng)求的來(lái)源:
- 第一次 (useQuery 自動(dòng)觸發(fā)): useQuery 在組件掛載時(shí),會(huì)使用初始的 queryKey 自動(dòng)發(fā)起一次請(qǐng)求。
- 第二次 (onMounted 手動(dòng)觸發(fā)): onMounted 鉤子中明確調(diào)用了 loadData()。
- 第三次 (watch(activeTab)): activeTab 在初始化時(shí),watch 監(jiān)聽(tīng)器被觸發(fā),調(diào)用了 loadData()。
- 第四次 (PageWrapper 初始化): 封裝的 <cec-page-wrapper> 組件在內(nèi)部的分頁(yè)器初始化時(shí),會(huì) emit 一次 @load-data 事件,再次調(diào)用了 loadData()。
根源: 我們犯了一個(gè)典型的錯(cuò)誤——不信任框架,手動(dòng)控制一切。我們?cè)噲D在每個(gè)可能的地方都調(diào)用 loadData,卻沒(méi)有意識(shí)到 useQuery 的響應(yīng)式 queryKey 已經(jīng)為我們處理了大部分情況,從而導(dǎo)致了多次重復(fù)的調(diào)用。
解決方案 (V1): 我們決定信任 useQuery,并精確控制請(qǐng)求時(shí)機(jī)。
- 禁用自動(dòng)請(qǐng)求: 給 useQuery 添加 enabled: false 選項(xiàng)。
- 添加 isMounted 守衛(wèi): 創(chuàng)建一個(gè) isMounted 標(biāo)志位,阻止分頁(yè)組件在 onMounted 完成前的初始化調(diào)用。
- 統(tǒng)一入口: 在 onMounted 的最后,手動(dòng)調(diào)用 refetch() 來(lái)發(fā)起唯一的第一次請(qǐng)求。
// ...
const { ..., refetch } = useQuery({ ..., enabled: false });
const isMounted = ref(false);
const handlePageChange = (pageInfo) => {
if (!isMounted.value) return; // 守衛(wèi)
// ...
refetch();
};
onMounted(async () => {
await getBusinessNodesList();
searchParams.value = cloneDeep(activeFilters.value);
await refetch(); // 手動(dòng)觸發(fā)
isMounted.value = true;
});
結(jié)果: 這個(gè)修改非常有效!請(qǐng)求次數(shù)從4次銳減到了2次。但為什么還有2次?
第二階段:從2次請(qǐng)求到1次 —— 深入響應(yīng)式依賴
我們?cè)俅螌彶榇a,發(fā)現(xiàn) onMounted 中手動(dòng)調(diào)用的 refetch() 仍然是多余的。盡管我們禁用了它,但 watch 和其他地方的邏輯仍然可能在初始化階段觸發(fā) refetch。
更深層次的根源: useQuery 的 queryKey 依賴于 searchParams。而 searchParams 的初始值是通過(guò) cloneDeep(activeFilters.value) 計(jì)算得來(lái)的。activeFilters 又依賴 filterParams 和 activeTab。整個(gè)依賴鏈條是這樣的:
activeTab -> activeFilters -> searchParams -> queryKey
在組件掛載的微任務(wù)隊(duì)列中,這些響應(yīng)式數(shù)據(jù)的初始化和 watch 的觸發(fā)順序存在我們未預(yù)料到的交互,導(dǎo)致了 searchParams 在短時(shí)間內(nèi)被更新了兩次。
最終的解決方案 (V2): 我們意識(shí)到,問(wèn)題的關(guān)鍵不是去“堵”住所有的觸發(fā)點(diǎn),而是讓觸發(fā)機(jī)制變得單一和可預(yù)測(cè)。
- 回歸 useQuery 的自動(dòng)行為: 我們移除 enabled: false,我們相信并利用它的自動(dòng)加載能力。
- 確保初始 queryKey 的穩(wěn)定性: 最重要的修改——在定義 searchParams 時(shí),就立即用穩(wěn)定的初始值 cloneDeep(activeFilters.value) 對(duì)其進(jìn)行初始化。
- 移除所有手動(dòng)的首次加載調(diào)用: 刪除 onMounted 中的 refetch() 或 search() 調(diào)用。
最終只有1次請(qǐng)求的代碼:
// 1. 狀態(tài)定義:searchParams 在定義時(shí)就擁有了正確的初始值
const activeTab = ref('PROVIDER');
const activeFilters = computed(() => filterParams[activeTab.value]);
const searchParams = ref(cloneDeep(activeFilters.value)); // 關(guān)鍵!
// 2. useQuery: 默認(rèn)啟用,它會(huì)在掛載時(shí)自動(dòng)使用上面的 searchParams 發(fā)起請(qǐng)求
const { isFetching, data: tableData, refetch } = useQuery({
queryKey: computed(() => ['transactionList', activeTab.value, ..., searchParams.value]),
queryFn: async () => { /* ... */ },
});
// 3. 事件處理:只負(fù)責(zé)更新?tīng)顟B(tài),不直接調(diào)用 refetch
const search = () => {
activeTableState.value.currentPage = 1;
// 只更新 searchParams,useQuery 會(huì)自動(dòng)響應(yīng) queryKey 的變化
searchParams.value = cloneDeep(activeFilters.value);
};
// 4. 生命周期:只做與列表數(shù)據(jù)無(wú)關(guān)的初始化
onMounted(() => {
getBusinessNodesList();
// 不需要任何 loadData 或 refetch 調(diào)用!
});
為什么這次能成功?
- 單一入口: useQuery 的 queryFn 現(xiàn)在是獲取數(shù)據(jù)的唯一入口。
- 單一觸發(fā)器: queryKey 的變化是觸發(fā) queryFn 的唯一方式。
- 可預(yù)測(cè)的狀態(tài): 在組件掛載時(shí),searchParams 的初始值是確定的、穩(wěn)定的。useQuery 使用這個(gè)穩(wěn)定的 key 發(fā)起了唯一一次初始請(qǐng)求。
- 清晰的職責(zé):
- 用戶的輸入只改變 activeFilters (UI狀態(tài))。
- 用戶的搜索動(dòng)作 (@search, @change, reset) 才去調(diào)用 search(),將 UI 狀態(tài)“提交”給 searchParams (查詢狀態(tài))。
- useQuery 忠實(shí)地響應(yīng) searchParams 的變化。
結(jié)論
還是要深入了解 Vue 3 響應(yīng)式系統(tǒng)和 vue-query 聲明式數(shù)據(jù)獲取的理念啊~~~不然有AI也要卡殼子,耽誤牛馬下班~
- 不要與框架對(duì)抗: 相信
useQuery的響應(yīng)式能力,避免在onMounted或watch中進(jìn)行手動(dòng)的、命令式的refetch調(diào)用。 - 分離狀態(tài): 將用于UI雙向綁定的狀態(tài)(如
activeFilters)和用于觸發(fā)數(shù)據(jù)請(qǐng)求的狀態(tài)(如searchParams)分離開(kāi),可以有效切斷意外的響應(yīng)式連鎖反應(yīng)。 - 穩(wěn)定
queryKey: 確保useQuery在首次自動(dòng)執(zhí)行時(shí),其queryKey所依賴的所有數(shù)據(jù)都已處于穩(wěn)定和正確的初始狀態(tài)。
通過(guò)遵循這些原則,我們可以構(gòu)建出既簡(jiǎn)潔又健壯的數(shù)據(jù)獲取邏輯,真正發(fā)揮出現(xiàn)代前端框架的威力。
到此這篇關(guān)于Vue3 + vue-query 的重復(fù)請(qǐng)求問(wèn)題解決 的文章就介紹到這了,更多相關(guān)vue-query 重復(fù)請(qǐng)求內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Vue 3 中使用路由參數(shù)跳轉(zhuǎn)時(shí) watch 觸發(fā)重復(fù)請(qǐng)求問(wèn)題解決
- vue?禁止重復(fù)點(diǎn)擊發(fā)送多次請(qǐng)求的實(shí)現(xiàn)
- Vue axios庫(kù)避免重復(fù)發(fā)送請(qǐng)求的示例介紹
- vue阻止重復(fù)請(qǐng)求實(shí)現(xiàn)示例詳解
- Vue路由切換和Axios接口取消重復(fù)請(qǐng)求詳解
- Vue中Axios中取消請(qǐng)求及阻止重復(fù)請(qǐng)求的方法
- vue axios攔截器常用之重復(fù)請(qǐng)求取消
- vue axios重復(fù)點(diǎn)擊取消上一次請(qǐng)求封裝的方法
相關(guān)文章
bootstrap基礎(chǔ)知識(shí)學(xué)習(xí)筆記
這篇文章主要針對(duì)bootstrap基礎(chǔ)知識(shí)為大家整理了詳細(xì)的學(xué)習(xí)筆記,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11
網(wǎng)易JS面試題與Javascript詞法作用域說(shuō)明
Javascript函數(shù)在定義它們的作用域里運(yùn)行,而不是在執(zhí)行它們的作用域里運(yùn)行。2010-11-11
基于VSCode調(diào)試網(wǎng)頁(yè)JavaScript代碼過(guò)程詳解
這篇文章主要介紹了基于VSCode調(diào)試網(wǎng)頁(yè)JavaScript代碼過(guò)程詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07
關(guān)于預(yù)加載InstantClick的問(wèn)題解決方法
本篇文章主要介紹了關(guān)于預(yù)加載InstantClick的問(wèn)題解決方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-09-09
Canvas實(shí)現(xiàn)動(dòng)態(tài)粒子文字效果的代碼示例
這篇文章主要介紹了如何用Canvas實(shí)現(xiàn)動(dòng)態(tài)粒子文字效果,文中有完整的代碼示例,文章通過(guò)代碼介紹的非常清楚,感興趣的小伙伴跟著小編一起來(lái)看看吧2023-08-08
可能被忽略的一些JavaScript數(shù)組方法細(xì)節(jié)
這篇文章主要給大家介紹了一些可能被忽略的JavaScript數(shù)組方法細(xì)節(jié),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02
JS實(shí)現(xiàn)鼠標(biāo)滑過(guò)折疊與展開(kāi)菜單效果代碼
這篇文章主要介紹了JS實(shí)現(xiàn)鼠標(biāo)滑過(guò)折疊與展開(kāi)菜單效果代碼,涉及JavaScript基于鼠標(biāo)事件動(dòng)態(tài)改變頁(yè)面元素樣式的相關(guān)實(shí)現(xiàn)技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-09-09
利用JavaScript實(shí)現(xiàn)時(shí)間戳功能的5種方法詳解
在現(xiàn)代軟件開(kāi)發(fā)中,時(shí)間戳是記錄時(shí)間的重要手段,在JavaScript中,通過(guò)不同的方法可以獲取當(dāng)前的時(shí)間戳,這篇文章主要介紹了利用JavaScript實(shí)現(xiàn)時(shí)間戳功能的5種方法,需要的朋友可以參考下2025-10-10

