
14. 请求去重 - 并发请求处理概述请求去重是一种优化技术用于防止相同请求同时发起多次避免浪费网络资源和增加服务器压力。常见于多个组件同时请求相同数据、用户快速重复点击等场景。维度内容What防止相同请求同时发起多次使用请求缓存或去重Why避免浪费网络资源和服务器压力提升性能When多个组件同时请求相同数据、快速重复点击WhereAPI 请求层、数据获取 Hook 中Who需要优化网络请求的开发者How使用 React Query 自动去重、手动实现请求缓存1. 为什么需要请求去重1.1 并发请求的问题// ❌ 问题多个组件同时请求相同数据 function App() { return ( div UserProfile / // 请求 /api/user UserStats / // 请求 /api/user UserSettings / // 请求 /api/user /div ); } // 结果三个组件各自发起一次请求 → 3 次重复请求1.2 快速重复点击的问题// ❌ 问题用户快速点击按钮 function SubmitButton() { const handleClick async () { await fetch(/api/submit, { method: POST }); }; return button onClick{handleClick}提交/button; } // 用户快速点击 5 次 → 5 次重复请求2. 请求去重的实现方式2.1 简单防抖Debounceimport { debounce } from lodash; function SearchInput() { const [query, setQuery] useState(); // 防抖延迟执行只执行最后一次 const debouncedSearch useCallback( debounce(async (value) { const response await fetch(/api/search?q${value}); const data await response.json(); console.log(搜索结果:, data); }, 500), [] ); const handleChange (e) { const value e.target.value; setQuery(value); debouncedSearch(value); }; return input value{query} onChange{handleChange} /; }2.2 简单节流Throttleimport { throttle } from lodash; function ScrollLoadMore() { const throttleLoadMore useCallback( throttle(async () { const response await fetch(/api/items?pagenext); const data await response.json(); console.log(加载更多:, data); }, 1000), [] ); useEffect(() { window.addEventListener(scroll, throttleLoadMore); return () window.removeEventListener(scroll, throttleLoadMore); }, []); }2.3 Promise 缓存核心去重// 全局 Promise 缓存 const pendingRequests new Map(); async function fetchWithDedupe(url, options {}) { const key ${options.method || GET}:${url}; // 如果已经有相同的请求在进行中返回已有的 Promise if (pendingRequests.has(key)) { console.log(复用进行中的请求:, key); return pendingRequests.get(key); } // 创建新请求 const promise fetch(url, options) .then(response response.json()) .finally(() { // 请求完成后清除缓存 pendingRequests.delete(key); }); pendingRequests.set(key, promise); return promise; } // 使用示例 async function getUserData(userId) { return fetchWithDedupe(/api/users/${userId}); } // 同时调用多次 Promise.all([ getUserData(1), getUserData(1), getUserData(1), ]).then(results { console.log(只发起了一次请求); });2.4 带超时的请求缓存class RequestDeduplicator { constructor(timeout 5000) { this.pending new Map(); this.timeout timeout; } async request(url, options {}) { const key ${options.method || GET}:${url}; if (this.pending.has(key)) { return this.pending.get(key); } const timeoutPromise new Promise((_, reject) { setTimeout(() reject(new Error(请求超时)), this.timeout); }); const fetchPromise fetch(url, options).then(res res.json()); const promise Promise.race([fetchPromise, timeoutPromise]) .finally(() { this.pending.delete(key); }); this.pending.set(key, promise); return promise; } clear() { this.pending.clear(); } } const deduper new RequestDeduplicator(3000);2.5 响应式缓存带 TTLclass CacheWithTTL { constructor(ttl 60000) { // 默认 1 分钟 this.cache new Map(); this.ttl ttl; this.pending new Map(); } async get(url, options {}) { const key ${options.method || GET}:${url}; const cached this.cache.get(key); // 检查缓存是否有效 if (cached Date.now() - cached.timestamp this.ttl) { console.log(从缓存返回:, key); return cached.data; } // 检查是否有进行中的请求 if (this.pending.has(key)) { console.log(复用进行中的请求:, key); return this.pending.get(key); } // 发起新请求 const promise fetch(url, options) .then(res res.json()) .then(data { // 存入缓存 this.cache.set(key, { data, timestamp: Date.now(), }); return data; }) .finally(() { this.pending.delete(key); }); this.pending.set(key, promise); return promise; } invalidate(url, options {}) { const key ${options.method || GET}:${url}; this.cache.delete(key); } clear() { this.cache.clear(); this.pending.clear(); } } const cache new CacheWithTTL(30000); // 30 秒缓存3. React Query 的自动去重3.1 内置去重机制import { useQuery } from tanstack/react-query; function UserProfile({ userId }) { // React Query 自动去重相同 queryKey 的请求只会发起一次 const { data } useQuery({ queryKey: [user, userId], queryFn: () fetchUser(userId), }); return div{data?.name}/div; } // 多个组件使用相同的 queryKey → 只发起一次请求 function App() { return ( UserProfile userId{1} / UserProfile userId{1} / UserProfile userId{1} / / ); }3.2 配置去重时间const queryClient new QueryClient({ defaultOptions: { queries: { staleTime: 5000, // 5 秒内数据新鲜不会重新请求 gcTime: 60000, // 缓存保留 1 分钟 refetchOnMount: false, // 组件挂载时不自动重新请求 }, }, }); // 特定查询的去重配置 const { data } useQuery({ queryKey: [user, userId], queryFn: () fetchUser(userId), staleTime: 30000, // 30 秒内不会重新请求 });4. SWR 的去重机制4.1 自动去重import useSWR from swr; function UserProfile({ userId }) { // SWR 自动去重相同 key 的请求只会发起一次 const { data } useSWR(/api/users/${userId}, fetcher); return div{data?.name}/div; } // 多个组件使用相同 key → 只发起一次请求 function App() { return ( UserProfile userId{1} / UserProfile userId{1} / UserProfile userId{1} / / ); }4.2 配置去重import { SWRConfig } from swr; function App() { return ( SWRConfig value{{ dedupingInterval: 2000, // 2 秒内重复请求会被去重 focusThrottleInterval: 5000, // 焦点重新请求的节流间隔 }} YourApp / /SWRConfig ); }5. 防止重复点击5.1 使用状态锁function SubmitButton() { const [isSubmitting, setIsSubmitting] useState(false); const handleSubmit async () { if (isSubmitting) return; // 防止重复提交 setIsSubmitting(true); try { await fetch(/api/submit, { method: POST }); alert(提交成功); } catch (error) { alert(提交失败); } finally { setIsSubmitting(false); } }; return ( button onClick{handleSubmit} disabled{isSubmitting} {isSubmitting ? 提交中... : 提交} /button ); }5.2 自定义 HookuseAsyncActionfunction useAsyncAction(asyncFn) { const [isLoading, setIsLoading] useState(false); const [error, setError] useState(null); const execute useCallback(async (...args) { if (isLoading) return; // 防止重复执行 setIsLoading(true); setError(null); try { const result await asyncFn(...args); return result; } catch (err) { setError(err); throw err; } finally { setIsLoading(false); } }, [asyncFn, isLoading]); return { execute, isLoading, error }; } // 使用 function LikeButton() { const { execute: like, isLoading } useAsyncAction(async () { await fetch(/api/like, { method: POST }); }); return ( button onClick{like} disabled{isLoading} {isLoading ? 处理中... : 点赞} /button ); }5.3 使用 AbortController 取消重复请求function SearchComponent() { const [query, setQuery] useState(); const [results, setResults] useState([]); const abortControllerRef useRef(null); const search useCallback(async (searchTerm) { // 取消上一次的请求 if (abortControllerRef.current) { abortControllerRef.current.abort(); } if (!searchTerm) { setResults([]); return; } const controller new AbortController(); abortControllerRef.current controller; try { const response await fetch(/api/search?q${searchTerm}, { signal: controller.signal, }); const data await response.json(); setResults(data); } catch (error) { if (error.name ! AbortError) { console.error(搜索失败:, error); } } }, []); const handleChange (e) { const value e.target.value; setQuery(value); search(value); }; return ( div input value{query} onChange{handleChange} / ul{results.map(item li key{item.id}{item.name}/li)}/ul /div ); }6. 高级去重策略6.1 请求优先级队列class RequestQueue { constructor() { this.queue []; this.processing new Set(); } async add(url, options {}, priority 0) { const key ${options.method || GET}:${url}; // 如果正在处理返回已有的 Promise if (this.processing.has(key)) { return this.processing.get(key); } return new Promise((resolve, reject) { this.queue.push({ url, options, priority, resolve, reject }); this.queue.sort((a, b) b.priority - a.priority); this.process(); }); } async process() { if (this.processing.size 3) return; // 最多 3 个并发 if (this.queue.length 0) return; const { url, options, resolve, reject } this.queue.shift(); const key ${options.method || GET}:${url}; const promise fetch(url, options) .then(res res.json()) .then(resolve) .catch(reject) .finally(() { this.processing.delete(key); this.process(); }); this.processing.set(key, promise); this.process(); } } const queue new RequestQueue();6.2 批量请求合并class BatchRequester { constructor(batchWindow 50) { // 50ms 窗口 this.batchWindow batchWindow; this.pending new Map(); this.timeouts new Map(); } request(key, fetcher) { return new Promise((resolve, reject) { if (!this.pending.has(key)) { this.pending.set(key, []); } this.pending.get(key).push({ resolve, reject }); if (!this.timeouts.has(key)) { const timeout setTimeout(() { this.flush(key); }, this.batchWindow); this.timeouts.set(key, timeout); } }); } async flush(key) { const callbacks this.pending.get(key); if (!callbacks || callbacks.length 0) return; this.pending.delete(key); this.timeouts.delete(key); // 批量获取数据 const results await this.fetchBatch(key, callbacks.length); // 分发给各个回调 callbacks.forEach(({ resolve, reject }, index) { if (results[index]) { resolve(results[index]); } else { reject(new Error(Batch request failed)); } }); } async fetchBatch(key, count) { // 示例批量获取用户数据 const ids key.split(,); const response await fetch(/api/users/batch?ids${ids.join(,)}); return response.json(); } } const batcher new BatchRequester(); // 使用多个请求会被合并为一个 async function getUser(id) { return batcher.request(user:${id}, () fetchUser(id)); }7. 完整示例用户列表去重import { useState, useEffect, useRef } from react; // 请求去重器 class RequestDeduplicator { constructor() { this.pending new Map(); } async fetch(url, options {}) { const key ${options.method || GET}:${url}; if (this.pending.has(key)) { console.log([去重] 复用请求: ${key}); return this.pending.get(key); } console.log([去重] 发起新请求: ${key}); const promise fetch(url, options) .then(res { if (!res.ok) throw new Error(HTTP ${res.status}); return res.json(); }) .finally(() { this.pending.delete(key); }); this.pending.set(key, promise); return promise; } } const deduper new RequestDeduplicator(); // API 函数 const api { getUser: (id) deduper.fetch(/api/users/${id}), getUsers: () deduper.fetch(/api/users), }; // 用户卡片组件 function UserCard({ userId }) { const [user, setUser] useState(null); const [loading, setLoading] useState(true); useEffect(() { let mounted true; api.getUser(userId) .then(data { if (mounted) { setUser(data); setLoading(false); } }) .catch(error { console.error(加载失败:, error); if (mounted) setLoading(false); }); return () { mounted false; }; }, [userId]); if (loading) return div加载用户 {userId}.../div; return div{user?.name}/div; } // 用户列表组件 function UserList() { const [users, setUsers] useState([]); const [loading, setLoading] useState(true); useEffect(() { api.getUsers() .then(data { setUsers(data); setLoading(false); }) .catch(error { console.error(加载列表失败:, error); setLoading(false); }); }, []); if (loading) return div加载用户列表.../div; return ( div h2用户列表/h2 {users.map(user ( UserCard key{user.id} userId{user.id} / ))} /div ); } export default UserList;8. 总结核心要点要点说明核心价值避免重复请求节省网络资源和服务器压力主要场景多组件并发请求、快速重复点击实现方式防抖/节流、Promise 缓存、状态锁现成方案React Query、SWR 内置去重选择策略场景推荐方案多组件并发请求React Query / SWR自动去重快速重复点击状态锁 disabled搜索输入防抖 AbortController滚动加载节流批量数据请求合并记忆口诀并发请求要去重Promise 缓存来帮忙重复点击状态锁搜索防抖加取消React Query 自动做省心省力性能强9. 相关资源MDN AbortControllerReact Query 去重文档SWR 去重配置