1. 由来
最近在公司的项目里面发现好多同事请求都在使用ahooks的useRequest
,发现这个自定义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>
- 更加详细的用法请参考useRequest基础用法
2.2 功能介绍
1. 功能
-
useRequest
是一个强大的异步数据管理的 Hooks,React 项目中的网络请求场景使用useRequest
就够了。useRequest
通过插件式组织代码,核心代码极其简单,并且可以很方便的扩展出更高级的功能。目前已有能力包括:- 自动请求/手动请求
- 轮询
- 防抖
- 节流
- 屏幕聚焦重新请求
- 错误重试
- loading delay
- SWR(stale-while-revalidate)
- 缓存
当然我们也可以通过自己编写插件去实现不同的功能,目前官方给出了这些已有的功能。
2. 插件生命周期
-
通过源码查看得知,useRequest的插件大概有如下几个生命周期(以及对应使用到的插件):
onInit
初始化时触发 -- useAutoRunPluginonBefore
在请求之前执行 -- useAutoRunPlugin、useCachePlugin、useLoadingDelayPlugin、usePollingPlugin、useRetryPluginonRequest
发起请求 -- useCachePluginonSuccess
请求成功时触发 -- useCachePlugin、useRetryPluginonError
请求失败时触发 -- useRetryPluginonFinally
请求完成时触发(类似于finally)-- useLoadingDelayPlugin、usePollingPluginonCancel
取消请求时触发 -- useDebouncePlugin、useLoadingDelayPlugin、usePollingPlugin、useRetryPlugin、useThrottlePluginonMutate
手动修改返回的数据时触发 -- useCachePlugin
3. 前置知识
useRequest源码里面也是用到了其他的自定义hooks可以提前了解一下
useCreation:useCreation 是 useMemo 或 useRef 的替代品。
useLatest:返回当前最新值的 Hook。
useMemoizedFn:持久化 function 的 Hook,理论上,可以使用 useMemoizedFn 完全代替 useCallback。
useMount:只在组件初始化时执行的 Hook。
useUnmount:在组件卸载(unmount)时执行的 Hook。
useUpdate:useUpdate 会返回一个函数,调用该函数会强制组件重新渲染。
2.3 代码实现
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 触发的请求都不会执行。
- 当
- 实例化请求
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(){...}
}
- 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
原理同上,使用的lodash
的throttle
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