ahooks的useRequest原理

6,488 阅读12分钟

1. 由来

最近在公司的项目里面发现好多同事请求都在使用ahooksuseRequest,发现这个自定义hook确实挺好用,就激发起了我想了解它是怎么实现的欲望。而且官方的实现思路和代码都很清晰很值得学习。通过自己的学习也想分享给大家,一起来了解一下useRequest的实现。

以下源码是基于ahooks 3.1.2版本

下面我们开始吧!

2. 实现原理

2.1 用法

 // 基本用法
 const { data, error, loading } = useRequest(service);
 ​
 // 手动控制
 const { loading, run, runAsync } = useRequest(service, {
   manual: true
 });
 ​
 <button onClick={run} disabled={loading}>
   {loading ? 'Loading' : 'Edit'}
 </button>

2.2 功能介绍

1. 功能

  • useRequest 是一个强大的异步数据管理的 Hooks,React 项目中的网络请求场景使用 useRequest 就够了。useRequest 通过插件式组织代码,核心代码极其简单,并且可以很方便的扩展出更高级的功能。目前已有能力包括:

    • 自动请求/手动请求
    • 轮询
    • 防抖
    • 节流
    • 屏幕聚焦重新请求
    • 错误重试
    • loading delay
    • SWR(stale-while-revalidate)
    • 缓存

当然我们也可以通过自己编写插件去实现不同的功能,目前官方给出了这些已有的功能。

2. 插件生命周期

  • 通过源码查看得知,useRequest的插件大概有如下几个生命周期(以及对应使用到的插件):

    • onInit初始化时触发 -- useAutoRunPlugin
    • onBefore在请求之前执行 -- useAutoRunPlugin、useCachePlugin、useLoadingDelayPlugin、usePollingPlugin、useRetryPlugin
    • onRequest发起请求 -- useCachePlugin
    • onSuccess请求成功时触发 -- useCachePlugin、useRetryPlugin
    • onError请求失败时触发 -- useRetryPlugin
    • onFinally请求完成时触发(类似于finally)-- useLoadingDelayPlugin、usePollingPlugin
    • onCancel取消请求时触发 -- useDebouncePlugin、useLoadingDelayPlugin、usePollingPlugin、useRetryPlugin、useThrottlePlugin
    • onMutate手动修改返回的数据时触发 -- useCachePlugin

3. 前置知识

useRequest源码里面也是用到了其他的自定义hooks可以提前了解一下

 useCreation:useCreation 是 useMemo 或 useRef 的替代品。
 useLatest:返回当前最新值的 Hook。
 useMemoizedFn:持久化 functionHook,理论上,可以使用 useMemoizedFn 完全代替 useCallback。
 useMount:只在组件初始化时执行的 Hook。
 useUnmount:在组件卸载(unmount)时执行的 Hook。
 useUpdate:useUpdate 会返回一个函数,调用该函数会强制组件重新渲染。

2.3 代码实现

1. 调用过程

  1. 首先当我们调用useRequest的时候
 // 其实useRequest整个初始化的代码就只有这点,因为这次官方将各个功能抽取成了插件的方式去实现
 function useRequest<TData, TParams extends any[]>(
   service: Service<TData, TParams>,
   options?: Options<TData, TParams>,
   plugins?: Plugin<TData, TParams>[],
 ) {
   return useRequestImplement<TData, TParams>(service, options, [
     ...(plugins || []), // 我们也可以传入自己的plugin
     useDebouncePlugin, // 防抖
     useLoadingDelayPlugin, // 延迟loading的状态
     usePollingPlugin, // 轮训
     useRefreshOnWindowFocusPlugin, // 窗口聚焦时重新请求
     useThrottlePlugin, // 节流
     useAutoRunPlugin, // 根据ready的变化自动请求
     useCachePlugin, // 缓存
     useRetryPlugin, // 错误重试
 ])
  • 具体实例化的方法
 function useRequestImplement<TData, TParams extends any[]>(
   service: Service<TData, TParams>,
   options: Options<TData, TParams> = {},
   plugins: Plugin<TData, TParams>[] = [],
 ) {
   // 默认是自动发送请求的
   const { manual = false, ...rest } = options;
   const fetchOptions = {
     manual,
     ...rest,
   };
   // 保存最新的请求方法的引用
   const serviceRef = useLatest(service);
   // 更新
   const update = useUpdate();
   // 创建请求实例
   const fetchInstance = useCreation(() => {
     // 运行每个插件的onInit方法
     const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
     return new Fetch<TData, TParams>(
       serviceRef,
       fetchOptions,
       update,
       Object.assign({}, ...initState),
     );
   }, []);
   fetchInstance.options = fetchOptions;
   // 运行所有plugin
   fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
 ​
   // 挂载的时候如果是自动的则发起请求
   useMount(() => {
     if (!manual) {
       // useCachePlugin can set fetchInstance.state.params from cache when init
       const params = fetchInstance.state.params || options.defaultParams || [];
       fetchInstance.run(...params);
     }
   });
 ​
   // 卸载的时候取消请求
   useUnmount(() => {
     fetchInstance.cancel();
   });
 ​
   // 返回数据
   return {
     loading: fetchInstance.state.loading,
     data: fetchInstance.state.data,
     error: fetchInstance.state.error,
     params: fetchInstance.state.params || [],
     cancel: useMemoizedFn(fetchInstance.cancel.bind(fetchInstance)),
     refresh: useMemoizedFn(fetchInstance.refresh.bind(fetchInstance)),
     refreshAsync: useMemoizedFn(fetchInstance.refreshAsync.bind(fetchInstance)),
     run: useMemoizedFn(fetchInstance.run.bind(fetchInstance)),
     runAsync: useMemoizedFn(fetchInstance.runAsync.bind(fetchInstance)),
     mutate: useMemoizedFn(fetchInstance.mutate.bind(fetchInstance)),
   };
 }
  • 这里我们看调用了onInit生命周期
  // 运行每个插件的onInit方法
 const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
 ​
 // 根据上面我们知道只有useAutoRunPlugin使用到了这个方法
 // 初始化loading的状态
 useAutoRunPlugin.onInit = ({ ready = true, manual }) => {
   return {
     loading: !manual && ready,
   };
 };

这里的ready会控制什么时候发起请求 useRequest 提供了一个options.ready 参数,当其值为 false 时,请求永远都不会发出。

  • 其具体行为如下:

    • manual=false 自动请求模式时,每次 ready 从 false 变为 true 时,都会自动发起请求,会带上参数 options.defaultParams。
    • manual=true 手动请求模式时,只要 ready=false,则通过 run/runAsync 触发的请求都不会执行。
  1. 实例化请求
 export default class Fetch<TData, TParams extends any[]> {
   // 所有的插件
   pluginImpls: PluginReturn<TData, TParams>[];
   // 计数器
   count: number = 0;
   // 初始数据
   state: FetchState<TData, TParams> = {
     loading: false,
     params: undefined,
     data: undefined,
     error: undefined,
   };
 ​
   constructor(
     public serviceRef: MutableRefObject<Service<TData, TParams>>,
     public options: Options<TData, TParams>,
     public subscribe: Subscribe,
     public initState: Partial<FetchState<TData, TParams>> = {},
   ) {
     this.state = {
       ...this.state,
       // 这里loading会根据是否是自动请求判断,后面也会被initState里面useAutoRunPlugin的onInit的返回状态决定
       loading: !options.manual,
       ...initState,
     };
   }
   // 这里的setState不是react class里面的setState,知识模拟了类似的实现
   setState(s: Partial<FetchState<TData, TParams>> = {}) {
     this.state = {
       ...this.state,
       ...s,
     };
     this.subscribe();
   }
   // 定义调用插件xx生命周期的公共方法
   runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
     // @ts-ignore
     const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);
     return Object.assign({}, ...r);
   }
   // 执行请求,也是我们使用时解构出来的run
   run(...params: TParams) {
     // 调用runAsync实现
     this.runAsync(...params)
       // 这也是为什么run会自动捕获异常的原因
       .catch((error) => {
       if (!this.options.onError) {
         console.error(error);
       }
     });
   }
   
   // 取消请求
   cancel() {
     this.count += 1;
     this.setState({
       loading: false,
     });
     // 调用插件的onCancel方法
     this.runPluginHandler('onCancel');
   }
  
   // 刷新其实就是重新请求
   refresh() {
     this.run(...(this.state.params || []));
   }
   
   // 同上
   refreshAsync() {
     return this.runAsync(...(this.state.params || []));
   }
   
   // 手动更改返回的数据
   mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
     let targetData: TData | undefined;
     if (typeof data === 'function') {
       targetData = data(this.state.data);
     } else {
       targetData = data;
     }
     // 调用插件的onMutate方法
     this.runPluginHandler('onMutate', targetData);
     this.setState({
       data: targetData,
     });
   }
   
   // 这个方法是真正处理所有逻辑的地方,所以单独拿出来看
   runAsync(){...}
   
 }
  1. runAsync的实现
 async runAsync(...params: TParams): Promise<TData> {
     // 计数器+1
     this.count += 1;
     const currentCount = this.count;
 ​
     const {
       stopNow = false, // !ready return true
       returnNow = false, // 如果缓存可以使用
       ...state // 如果有缓存这里的值会设置为缓存的值(不管有没有过期)
     } = this.runPluginHandler('onBefore', params);
 ​
     // stop request
     if (stopNow) {
       return new Promise(() => {});
     }
 ​
     this.setState({
       loading: true,
       params,
       ...state,
     });
 ​
     // 使用缓存
     if (returnNow) {
       return Promise.resolve(state.data);
     }
     // 调用自己传入的onBefore
     this.options.onBefore?.(params);
 ​
     try {
       // replace service
       // 后面会讲到与缓存有关
       let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);
 ​
       if (!servicePromise) {
         // 调用者传入的service
         servicePromise = this.serviceRef.current(...params);
       }
 ​
       const res = await servicePromise;
       
       // 这里的count在没次run和cancel都会+1,如果在请求之前没有调用cancel,那么两次的count是相等的
       if (currentCount !== this.count) {
         // prevent run.then when request is canceled
         return new Promise(() => {});
       }
       
       // 返回请求回来的数据 
       this.setState({
         data: res,
         error: undefined,
         loading: false,
       });
 ​
       // 调用onSuccess生命周期
       this.options.onSuccess?.(res, params);
       this.runPluginHandler('onSuccess', res, params);
       
       // 调用onFinally生命周期
       this.options.onFinally?.(params, res, undefined);
       if (currentCount === this.count) {
         this.runPluginHandler('onFinally', params, res, undefined);
       }
 ​
       return res;
     } catch (error) {
       if (currentCount !== this.count) {
         // prevent run.then when request is canceled
         return new Promise(() => {});
       }
 ​
       this.setState({
         error,
         loading: false,
       });
 ​
       // 调用onError生命周期
       this.options.onError?.(error, params);
       this.runPluginHandler('onError', error, params);
       
       // 调用onFinally生命周期
       this.options.onFinally?.(params, undefined, error);
       if (currentCount === this.count) {
         this.runPluginHandler('onFinally', params, undefined, error);
       }
 ​
       throw error;
     }
 }
  • 对于onRequest生命周期只有useCachePlugin调用了,我们来看一下
 const currentPromiseRef = useRef<Promise<any>>();
 ​
 // 如果使用了缓存那么也会缓存每次的service
 useCachePlugin.onRequest = (service, args) => {
   let servicePromise = cachePromise.getCachePromise(cacheKey);
   // If has servicePromise, and is not trigger by self, then use it
   if (servicePromise && servicePromise !== currentPromiseRef.current) {
     return { servicePromise };
   }
   servicePromise = service(...args);
   currentPromiseRef.current = servicePromise;
   cachePromise.setCachePromise(cacheKey, servicePromise);
   return { servicePromise };
 };
小结
  • 上面是主要的流程,可以看到主流程里面除了调用各种插件的生命周期,还有就是根据条件发起请求。就没有其他的事情了。因为这次版本管方把所有的功能都放到了各个插件去实现,这样不仅使代码变得更加简单易懂,而且每个插件负责一件事情也方便使用者自己去扩展功能。

2. 插件功能介绍

1. 自动请求/手动请求

上面我们已经看到了,如果manual为false的话就会自动发起请求,否则需要用户自己出触发

2. useAutoRunPlugin
 import { useRef } from 'react';
 import useUpdateEffect from '../../../useUpdateEffect';
 ​
 // support refreshDeps & ready
 const useAutoRunPlugin: Plugin<any, any[]> = (
   fetchInstance,
   // 用户传入的options
   { manual, ready = true, defaultParams = [], refreshDeps = [], refreshDepsAction },
 ) => {
   const hasAutoRun = useRef(false);
   hasAutoRun.current = false;
 ​
   // 可以理解为useEffect
   useUpdateEffect(() => {
     // 当 manual=false 自动请求模式时,每次 ready 从 false 变为 true 时,都会自动发起请求
     if (!manual && ready) {
       hasAutoRun.current = true;
       fetchInstance.run(...defaultParams);
     }
   }, [ready]);
 ​
   useUpdateEffect(() => {
     if (hasAutoRun.current) {
       return;
     }
     if (!manual) {
       hasAutoRun.current = true;
       if (refreshDepsAction) {
         refreshDepsAction();
       } else {
         fetchInstance.refresh();
       }
     }
   }, [...refreshDeps]); // 自己也可以定制deps,然后也可以自己处理回调
 ​
   return {
     onBefore: () => {
       if (!ready) {
         return {
           stopNow: true,
         };
       }
     },
   };
 };
 ​
 useAutoRunPlugin.onInit = ({ ready = true, manual }) => {
   return {
     loading: !manual && ready,
   };
 };
 ​
 export default useAutoRunPlugin;
3. useLoadingDelayPlugin
 import { useRef } from 'react';
 // 通过设置 options.loadingDelay ,可以延迟 loading 变成 true 的时间,有效防止闪烁。
 // 整体思想其实就是使用setTimeout延迟loading变为true的时间
 const useLoadingDelayPlugin: Plugin<any, any[]> = (fetchInstance, { loadingDelay }) => {
   const timerRef = useRef<Timeout>();
 ​
   if (!loadingDelay) {
     return {};
   }
 ​
   const cancelTimeout = () => {
     if (timerRef.current) {
       clearTimeout(timerRef.current);
     }
   };
 ​
   return {
     onBefore: () => {
       cancelTimeout();
       
       // 主要实现
       timerRef.current = setTimeout(() => {
         fetchInstance.setState({
           loading: true,
         });
       }, loadingDelay);
 ​
       return {
         loading: false,
       };
     },
     onFinally: () => {
       cancelTimeout();
     },
     onCancel: () => {
       cancelTimeout();
     },
   };
 };
 ​
 export default useLoadingDelayPlugin;
4. useDebouncePlugin
 import type { DebouncedFunc, DebounceSettings } from 'lodash';
 import debounce from 'lodash/debounce';
 import { useEffect, useMemo, useRef } from 'react';
 ​
 // 通过设置 options.debounceWait,进入防抖模式
 // 其实就是使用lodash的debounce实现的
 const useDebouncePlugin: Plugin<any, any[]> = (
   fetchInstance,
   { debounceWait, debounceLeading, debounceTrailing, debounceMaxWait },
 ) => {
   const debouncedRef = useRef<DebouncedFunc<any>>();
 ​
   // 因为这些参数支持动态变化
   const options = useMemo(() => {
     const ret: DebounceSettings = {};
     if (debounceLeading !== undefined) {
       ret.leading = debounceLeading;
     }
     if (debounceTrailing !== undefined) {
       ret.trailing = debounceTrailing;
     }
     if (debounceMaxWait !== undefined) {
       ret.maxWait = debounceMaxWait;
     }
     return ret;
   }, [debounceLeading, debounceTrailing, debounceMaxWait]);
 ​
   useEffect(() => {
     // 开启防抖
     if (debounceWait) {
       // 这里对原来的方法做了一次拦截
       const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
       // debounce runAsync should be promise
       // https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
       // 根据上面的issue,所以采用下面的方式进行处理debounce返回promise的问题
       fetchInstance.runAsync = (...args) => {
         return new Promise((resolve, reject) => {
           debouncedRef.current?.(() => {
             _originRunAsync(...args)
               .then(resolve)
               .catch(reject);
           });
         });
       };
       
       debouncedRef.current = debounce(
         (callback) => {
           callback();
         },
         debounceWait,
         options,
       );
 ​
       return () => {
         debouncedRef.current?.cancel();
         // 取消拦截
         fetchInstance.runAsync = _originRunAsync;
       };
     }
   }, [debounceWait, options]);
 ​
   if (!debounceWait) {
     return {};
   }
 ​
   return {
     onCancel: () => {
       debouncedRef.current?.cancel();
     },
   };
 };
 ​
 export default useDebouncePlugin;
5. useThrottlePlugin

原理同上,使用的lodashthrottle

6. usePollingPlugin
 import { useRef } from 'react';
 import useUpdateEffect from '../../../useUpdateEffect';
 import isDocumentVisible from '../utils/isDocumentVisible';
 import subscribeReVisible from '../utils/subscribeReVisible';
 ​
 // 通过设置 options.pollingInterval,进入轮询模式
 // 主体实现思想:使用setTimeout来实现轮询,轮询原理是在每次请求完成后,等待 pollingInterval 时间,发起下一次请求。
 const usePollingPlugin: Plugin<any, any[]> = (
   fetchInstance,
   { pollingInterval, pollingWhenHidden = true /*在页面隐藏时,是否继续轮询*/ },
 ) => {
   const timerRef = useRef<Timeout>();
   const unsubscribeRef = useRef<() => void>();
 ​
   // 停止轮询clearTimeout
   const stopPolling = () => {
     if (timerRef.current) {
       clearTimeout(timerRef.current);
     }
     unsubscribeRef.current?.();
   };
 ​
   // 支持动态变化
   useUpdateEffect(() => {
     if (!pollingInterval) {
       stopPolling();
     }
   }, [pollingInterval]);
 ​
   if (!pollingInterval) {
     return {};
   }
 ​
   return {
     onBefore: () => {
       stopPolling();
     },
     // 每次请求完毕都会调用onFinally生命周期
     onFinally: () => {
       // if pollingWhenHidden = false && document is hidden, then stop polling and subscribe revisible
       if (!pollingWhenHidden && !isDocumentVisible()) {
         unsubscribeRef.current = subscribeReVisible(() => {
           fetchInstance.refresh();
         });
         return;
       }
 ​
       timerRef.current = setTimeout(() => {
         fetchInstance.refresh();
       }, pollingInterval);
     },
     onCancel: () => {
       stopPolling();
     },
   };
 };
 ​
 export default usePollingPlugin;
 ​
 // isDocumentVisible.ts
 export default function isDocumentVisible(): boolean {
   if (canUseDom()) {
     return document.visibilityState !== 'hidden';
   }
   return true;
 }
 ​
 // subscribeReVisible.ts
 // 其实就是类似订阅模式,当visibilitychange并且visible的时候通知订阅的组件
 import isDocumentVisible from './isDocumentVisible';
 ​
 const listeners: any[] = [];
 function subscribe(listener: () => void) {
   listeners.push(listener);
   return function unsubscribe() {
     const index = listeners.indexOf(listener);
     listeners.splice(index, 1);
   };
 }
 ​
 if (canUseDom()) {
   const revalidate = () => {
     if (!isDocumentVisible()) return;
     for (let i = 0; i < listeners.length; i++) {
       const listener = listeners[i];
       listener();
     }
   };
   window.addEventListener('visibilitychange', revalidate, false);
 }
 ​
 export default subscribe;
7. useRefreshOnWindowFocusPlugin
 import { useEffect, useRef } from 'react';
 import limit from '../utils/limit'; // 可以理解为节流
 ​
 // 实现类似subscribeReVisible.ts只不过多了个focus事件,判断条件多了个是否isOnline(navigator.onLine)
 import subscribeFocus from '../utils/subscribeFocus';
 ​
 // 通过设置 options.refreshOnWindowFocus,在浏览器窗口 refocus 和 revisible 时,会重新发起请求,支持动态变化
 // 监听的浏览器事件为 visibilitychange 和 focus
 const useRefreshOnWindowFocusPlugin: Plugin<any, any[]> = (
   fetchInstance,
   { refreshOnWindowFocus, focusTimespan = 5000 /*重新请求间隔,单位为毫秒*/ },
 ) => {
   const unsubscribeRef = useRef<() => void>();
 ​
   const stopSubscribe = () => {
     unsubscribeRef.current?.();
   };
 ​
   useEffect(() => {
     if (refreshOnWindowFocus) {
       const limitRefresh = limit(fetchInstance.refresh.bind(fetchInstance), focusTimespan);
       unsubscribeRef.current = subscribeFocus(() => {
         limitRefresh();
       });
     }
     return () => {
       stopSubscribe();
     };
   }, [refreshOnWindowFocus, focusTimespan]);
 ​
   useUnmount(() => {
     stopSubscribe();
   });
 ​
   return {};
 };
 ​
 export default useRefreshOnWindowFocusPlugin;
 ​
8. useRetryPlugin
 import { useRef } from 'react';
 ​
 // 通过设置 options.retryCount,指定错误重试次数,则 useRequest 在失败后会进行重试。支持动态变化
 // 原来也是当发生错误的时候使用setTimeout设置不同的time(最大30s)进行请求
 const useRetryPlugin: Plugin<any, any[]> = (fetchInstance, { retryInterval, retryCount }) => {
   const timerRef = useRef<Timeout>();
   const countRef = useRef(0);
 ​
   const triggerByRetry = useRef(false);
 ​
   if (!retryCount) {
     return {};
   }
 ​
   return {
     onBefore: () => {
       if (!triggerByRetry.current) {
         // retry的次数
         countRef.current = 0;
       }
       triggerByRetry.current = false;
 ​
       if (timerRef.current) {
         clearTimeout(timerRef.current);
       }
     },
     onSuccess: () => {
       countRef.current = 0;
     },
     onError: () => {
       // 重试次数+1
       countRef.current += 1;
       // -1代表无限次重试
       if (retryCount === -1 || countRef.current <= retryCount) {
         // Exponential backoff
         const timeout = retryInterval ?? Math.min(1000 * 2 ** countRef.current, 30000);
         timerRef.current = setTimeout(() => {
           triggerByRetry.current = true;
           fetchInstance.refresh();
         }, timeout);
       } else {
         countRef.current = 0;
       }
     },
     onCancel: () => {
       countRef.current = 0;
       if (timerRef.current) {
         clearTimeout(timerRef.current);
       }
     },
   };
 };
 ​
 export default useRetryPlugin;
 ​
9. useCachePlugin
 import { useRef } from 'react';
 import * as cache from '../utils/cache';
 import * as cachePromise from '../utils/cachePromise';
 import * as cacheSubscribe from '../utils/cacheSubscribe';
 /**
 如果设置了 options.cacheKey,useRequest 会将当前请求成功的数据缓存起来。下次组件初始化时,如果有缓存数据,我们会优先返回缓存数据,然后在背后发送新请求,也就是 SWR 的能力。
 ​
 你可以通过 options.staleTime 设置数据保持新鲜时间,在该时间内,我们认为数据是新鲜的,不会重新发起请求。
 ​
 你也可以通过 options.cacheTime 设置数据缓存时间,超过该时间,我们会清空该条缓存数据。
 */
 const useCachePlugin: Plugin<any, any[]> = (
   fetchInstance,
   {
     cacheKey,
     cacheTime = 5 * 60 * 1000,
     staleTime = 0,
     setCache: customSetCache, // 自定义方法
     getCache: customGetCache, // 自定义方法,可以将数据存储到 localStorage、IndexDB 等
   },
 ) => {
   const unSubscribeRef = useRef<() => void>();
 ​
   const currentPromiseRef = useRef<Promise<any>>();
  
   // 设置缓存走自定义的还是默认的
   // 在自定义缓存模式下,cacheTime 和 clearCache 不会生效,请根据实际情况自行实现
   // 根据代码可以看到根本没有传递给自定义的方法,也就是说全部交给开发者实现
   const _setCache = (key: string, cachedData: CachedData) => {
     if (customSetCache) {
       customSetCache(cachedData);
     } else {
       cache.setCache(key, cacheTime, cachedData);
     }
     cacheSubscribe.trigger(key, cachedData.data);
   };
   
   // 获取缓存走自定义的还是默认的
   const _getCache = (key: string, params: any[] = []) => {
     if (customGetCache) {
       return customGetCache(params);
     }
     return cache.getCache(key);
   };
 ​
   useCreation(() => {
     // 没有cacheKey return
     if (!cacheKey) {
       return;
     }
 ​
     // get data from cache when init
     const cacheData = _getCache(cacheKey);
     if (cacheData && Object.hasOwnProperty.call(cacheData, 'data')) {
       // 去缓存并且赋值给state
       fetchInstance.state.data = cacheData.data;
       fetchInstance.state.params = cacheData.params;
       // 没有过期或者说是新鲜的数据
       if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
         fetchInstance.state.loading = false;
       }
     }
 ​
     // subscribe same cachekey update, trigger update
     /** 
       同一个 cacheKey 的内容,在全局是共享的,这会带来以下几个特性:  
       1. 请求 Promise 共享,相同的 cacheKey 同时只会有一个在发起请求,后发起的会共用同一个请求 Promise
       2. 数据同步,任何时候,当我们改变其中某个 cacheKey 的内容时,其它相同 cacheKey 的内容均会同步
     */
     unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (data) => {
       fetchInstance.setState({ data });
     });
   }, []);
 ​
   useUnmount(() => {
     unSubscribeRef.current?.();
   });
 ​
   if (!cacheKey) {
     return {};
   }
 ​
   return {
     onBefore: (params) => {
       const cacheData = _getCache(cacheKey, params);
 ​
       if (!cacheData || !Object.hasOwnProperty.call(cacheData, 'data')) {
         return {};
       }
       
       // 不管数据是不是新鲜的都会赋值给data只不过不新鲜的还会继续请求(设置缓存的情况下) 
       // If the data is fresh, stop request
       if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
         return {
           loading: false,
           data: cacheData?.data,
           returnNow: true,
         };
       } else {
         // If the data is stale, return data, and request continue
         return {
           data: cacheData?.data,
         };
       }
     },
     onRequest: (service, args) => {
       let servicePromise = cachePromise.getCachePromise(cacheKey);
 ​
       // If has servicePromise, and is not trigger by self, then use it
       if (servicePromise && servicePromise !== currentPromiseRef.current) {
         return { servicePromise };
       }
 ​
       servicePromise = service(...args);
       currentPromiseRef.current = servicePromise;
       // 根据key缓存promise也就是特性1
       cachePromise.setCachePromise(cacheKey, servicePromise);
       return { servicePromise };
     },
     onSuccess: (data, params) => {
       // 只有成功的数据才会缓存
       if (cacheKey) {
         // cancel subscribe, avoid trgger self
         unSubscribeRef.current?.();
         _setCache(cacheKey, {
           data,
           params, // 缓存params
           time: new Date().getTime(),
         });
         // resubscribe
         unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (d) => {
           fetchInstance.setState({ data: d });
         });
       }
     },
     // 手动修改数据时同onSuccess
     onMutate: (data) => {
       if (cacheKey) {
         // cancel subscribe, avoid trgger self
         unSubscribeRef.current?.();
         _setCache(cacheKey, {
           data,
           params: fetchInstance.state.params,
           time: new Date().getTime(),
         });
         // resubscribe
         unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (d) => {
           fetchInstance.setState({ data: d });
         });
       }
     },
   };
 };
 ​
 export default useCachePlugin;
  • 下面我们来看一下cache plugin里面util的实现
 // cache.ts 
 const cache = new Map<CachedKey, RecordData>();
 ​
 // 其实是使用一个Map存储数据,并且使用setTimeout来定期清除数据
 const setCache = (key: CachedKey, cacheTime: number, cachedData: CachedData) => {
   const currentCache = cache.get(key);
   // 如果有计数器清除
   if (currentCache?.timer) {
     clearTimeout(currentCache.timer);
   }
   let timer: Timer | undefined = undefined;
   // 如果缓存数据不是永不过期的
   if (cacheTime > -1) {
     // if cache out, clear it
     timer = setTimeout(() => {
       cache.delete(key);
     }, cacheTime);
   }
 ​
   cache.set(key, {
     ...cachedData,
     timer, // 定时器
   });
 };
 ​
 const getCache = (key: CachedKey) => {
   return cache.get(key);
 };
 ​
 // 清除缓存
 const clearCache = (key?: string | string[]) => {
   if (key) {
     const cacheKeys = Array.isArray(key) ? key : [key];
     cacheKeys.forEach((cacheKey) => cache.delete(cacheKey));
   } else {
     cache.clear();
   }
 };
 ​
 export { getCache, setCache, clearCache };
 ​
 // cachePromise.ts
 const cachePromise = new Map<CachedKey, Promise<any>>();
 ​
 const getCachePromise = (cacheKey: CachedKey) => {
   return cachePromise.get(cacheKey);
 };
 ​
 const setCachePromise = (cacheKey: CachedKey, promise: Promise<any>) => {
   // Should cache the same promise, cannot be promise.finally
   // Because the promise.finally will change the reference of the promise
   cachePromise.set(cacheKey, promise);
 ​
   // no use promise.finally for compatibility(兼容性)
   // 请求完之后删除自己,这样也保证了请求 Promise 共享
   promise
     .then((res) => {
       cachePromise.delete(cacheKey);
       return res;
     })
     .catch((err) => {
       cachePromise.delete(cacheKey);
       throw err;
     });
 };
 ​
 export { getCachePromise, setCachePromise };
 ​
 // cacheSubscribe.ts
 // 发布订阅模式
 const listeners: Record<string, Listener[]> = {};
 ​
 const trigger = (key: string, data: any) => {
   if (listeners[key]) {
     listeners[key].forEach((item) => item(data));
   }
 };
 ​
 const subscribe = (key: string, listener: Listener) => {
   if (!listeners[key]) {
     listeners[key] = [];
   }
   listeners[key].push(listener);
 ​
   return function unsubscribe() {
     const index = listeners[key].indexOf(listener);
     listeners[key].splice(index, 1);
   };
 };
 ​
 export { trigger, subscribe };

3. 总结

  • 以上就是useRequest全部的内容了,有了这样的自定义hooks我们确实在开发的时候少了很多的模板代码,对于一些常用的功能也不用自己去封装了。
  • 如果还想查看其他请求库的实现原理,可以看一下我其他的文章axios原理浅析--悄咪咪的带上umi-request