前言
本文是 ahooks 源码(v3.7.4)系列的第十四篇——【解读 ahooks 源码系列】探究如何实现 useRequest
往期文章:
- 【解读 ahooks 源码系列】(开篇)如何获取和监听 DOM 元素:useEffectWithTarget
- 【解读 ahooks 源码系列】DOM 篇(一):useEventListener、useClickAway、useDocumentVisibility、useDrop、useDrag
- 【解读 ahooks 源码系列】DOM 篇(二):useEventTarget、useExternal、useTitle、useFavicon、useFullscreen、useHover
- 【解读 ahooks 源码系列】DOM 篇(三):useMutationObserver、useInViewport、useKeyPress、useLongPress
- 【解读 ahooks 源码系列】DOM 篇(四):useMouse、useResponsive、useScroll、useSize、useFocusWithin
- 【解读 ahooks 源码系列】Dev 篇——useTrackedEffect 和 useWhyDidYouUpdate
- 【解读 ahooks 源码系列】Advanced 篇:useControllableValue、useCreation、useIsomorphicLayoutEffect、useEventEmitter、useLatest、useMemoizedFn、useReactive
- 【解读 ahooks 源码系列】State 篇(一):useSetState、useToggle、useBoolean、useCookieState、useLocalStorageState、useSessionStorageState、useDebounce、useThrottle
- 【解读 ahooks 源码系列】State 篇(二):useMap、useSet、usePrevious、useRafState、useSafeState、useGetState、useResetState
- 【解读 ahooks 源码系列】Effect 篇(一):useUpdateEffect、useUpdateLayoutEffect、useAsyncEffect、useDebounceFn、useDebounceEffect、useThrottleFn、useThrottleEffect
- 【解读 ahooks 源码系列】Effect 篇(二):useDeepCompareEffect、useDeepCompareLayoutEffect、useInterval、useTimeout、useRafInterval、useRafTimeout、useLockFn、useUpdate、useThrottleEffect
- 【解读 ahooks 源码系列】LifeCycle 篇 与 Scene 篇(一):useMount、useUnmount、useUnmountedRef、useCounter、useNetwork、useSelections、useHistoryTravel
- 【解读 ahooks 源码系列】 Scene 篇(二):useTextSelection、useCountdown、useDynamicList、useWebSocket
本文主要解读 useRequest
的源码实现。
useRequest
useRequest
是一个强大的异步数据管理的 Hooks,React 项目中的网络请求场景使用 useRequest
就够了。
useRequest
通过插件式组织代码,核心代码极其简单,并且可以很方便的扩展出更高级的功能。目前已有能力包括:
-
自动请求/手动请求
-
轮询
-
防抖
-
节流
-
屏幕聚焦重新请求
-
错误重试
-
loading delay
-
SWR(stale-while-revalidate)
-
缓存
基本用法
useRequest
的第一个参数是一个异步函数,在组件初次加载时,会自动触发该函数执行。同时自动管理该异步函数的 loading
, data
, error
等状态。
import { useRequest } from 'ahooks';
import Mock from 'mockjs';
import React from 'react';
function getUsername(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(Mock.mock('@name'));
}, 1000);
});
}
export default () => {
const { data, error, loading } = useRequest(getUsername);
if (error) {
return <div>failed to load</div>;
}
if (loading) {
return <div>loading...</div>;
}
return <div>Username: {data}</div>;
};
核心实现
主调用流程
- useRequest 负责初始化和处理数据,最后将结果返回
- useRequest 方法的入口文件
useRequest.ts
封装了useRequestImplement
这个 Hook,并引入了默认插件。
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 || []),
// 默认插件
useDebouncePlugin,
useLoadingDelayPlugin,
usePollingPlugin,
useRefreshOnWindowFocusPlugin,
useThrottlePlugin,
useAutoRunPlugin,
useCachePlugin,
useRetryPlugin,
] as Plugin<TData, TParams>[]);
}
useRequestImplement.ts
:实例化 Fetch
类——fetchInstance
,并针对实例的(卸载)生命周期相关进行逻辑处理
function useRequestImplement<TData, TParams extends any[]>(
service: Service<TData, TParams>,
options: Options<TData, TParams> = {},
plugins: Plugin<TData, TParams>[] = [],
) {
// manual:初始化时是否自动执行 service,默认为false
const { manual = false, ...rest } = options;
const fetchOptions = {
manual,
...rest,
};
const serviceRef = useLatest(service);
const update = useUpdate();
// fetch 请求实例
// useCreation, useMemo/useCallback 替代品,可以保证被 memo 的值不会被重新计算
const fetchInstance = useCreation(() => {
const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
return new Fetch<TData, TParams>(
serviceRef,
fetchOptions,
update,
Object.assign({}, ...initState),
);
}, []);
fetchInstance.options = fetchOptions;
// run all plugins hooks
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 || [];
// @ts-ignore
fetchInstance.run(...params);
}
});
useUnmount(() => {
fetchInstance.cancel();
});
// 向外抛出的方法都是 Fetch 实例的变量方法
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)),
} as Result<TData, TParams>;
}
可以看出,我们平时使用 useRequest 暴露出来的方法,几乎都是 fetchInstance 实例暴露出来的,所以源代码核心,也就是 Fetch
类了
核心 Fetch 类
Fetch 类是整个 Hook 的核心代码,它封装了暴露给外部的具体数据和方法,只需完成整体请求流程的功能即可。
先来看看这个类的大致结构
class Fetch<TData, TParams extends any[]> {
// 所有插件执行完成后的返回结果数组
pluginImpls: PluginReturn<TData, TParams>[];
count: number = 0;
state: FetchState<TData, TParams> = {
loading: false, // service 是否正在执行
params: undefined, // 当次执行的 service 的参数数组
data: undefined, // service 返回的数据
error: undefined, // service 抛出的异常
};
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: !options.manual,
...initState,
};
}
// 更新状态
setState(s: Partial<FetchState<TData, TParams>> = {}) {
// ...
}
// 插件运行处理
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
// ...
}
// 与 run 用法一致,但返回的是 Promise,需要自行处理异常
async runAsync(...params: TParams): Promise<TData> {
// ...
}
// 手动触发 service 执行,异常自动处理
run(...params: TParams) {
// ...
}
// 忽略当前 Promise 的响应
cancel() {
// ...
}
// 使用上一次的 params,重新调用 run
refresh() {
// ...
}
// 使用上一次的 params,重新调用 runAsync
refreshAsync() {
// ...
}
// 直接修改 data
mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
// ...
}
}
setState
该方法保存于请求参数和请求结果相关的信息,而这些是我们使用 useRequest 需要用的信息,如下:
const { data, error, loading } = useRequest(getUsername);
setState
实现如下,其中 subscribe
在 Fetch 实例化的时候传入的参数为 useUpdate
执行后返回的 update
方法
setState(s: Partial<FetchState<TData, TParams>> = {}) {
this.state = {
...this.state,
...s,
};
this.subscribe(); // 强制组件重新渲染
}
runPluginHandler
runPluginHandler
用于特定的生命周期钩子执行插件方法。该方法通过pluginImpls
数组循环找出所有对应 event
的钩子函数去执行并返回结果。
pluginImpls
是一个数组,用来存储所有插件 hooks 的返回结果。它的赋值是在 useRequestImplement.ts
实例化 Fetch 类后执行 fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
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);
}
这个运行插件的方法具体怎么理解作用呢?当我们想实现一个 useRequest
插件类实现特定功能时,想在接口请求成功时做一些事情,那在插件类里面要怎么知道呢?一种比较好的方式就是生命周期,我只要在插件类里写下 onSuccess
方法,在这里写逻辑就能轻松实现想要的功能而不用考虑其它。
核心思想就是,Fetch 类只需完成请求整体的流程,其它功能交给插件去实现。那如何把请求前后的流程分享给插件,那就是定义固定的生命周期钩子。如插件想要在接口请求成功后做些事情,则这里的处理只需通过插件数组循环找出哪个插件有写 onSuccess
这个钩子,有就找出来把它执行了。
来看下 PluginReturn
这个 TS 定义,也就是插件支持的钩子函数了
export interface PluginReturn<TData, TParams extends any[]> {
onBefore?: (params: TParams) =>
| ({
stopNow?: boolean;
returnNow?: boolean;
} & Partial<FetchState<TData, TParams>>)
| void;
onRequest?: (
service: Service<TData, TParams>,
params: TParams,
) => {
servicePromise?: Promise<TData>;
};
onSuccess?: (data: TData, params: TParams) => void;
onError?: (e: Error, params: TParams) => void;
onFinally?: (params: TParams, data?: TData, e?: Error) => void;
onCancel?: () => void;
onMutate?: (data: TData) => void;
}
runAsync
这个是 Fetch 类请求的核心方法,下面介绍的 run/refresh/refreshAsync
底层都是调用该方法,在该方法不同的时机也会调用插件的不同生命周期钩子函数。
onBefore 请求前
发起请求前,会调用插件的 onBefore
方法
async runAsync(...params: TParams): Promise<TData> {
this.count += 1;
const currentCount = this.count;
// onBefore 函数的返回值(如需实现特定功能插件,字段还可继续扩展)
const {
stopNow = false, // 是否停止请求(可选)- useAutoRunPlugin 插件使用
returnNow = false, // 是否立即返回(可选)- useCachePlugin 插件使用
...state
} = this.runPluginHandler('onBefore', params); // 执行每个插件的 onBefore 钩子函数
// stop request
if (stopNow) {
return new Promise(() => {});
}
this.setState({
loading: true, // 请求前把 loading 设置为 true
params,
...state,
});
// 是否立即返回
if (returnNow) {
return Promise.resolve(state.data);
}
// 执行外部传入的 onBefore 回调
this.options.onBefore?.(params);
// ...
}
onRequest 请求中
请求阶段
这个阶段只有 useCachePlugin 执行了 onRequest 方法,执行后返回 service Promise(有可能是缓存的结果),从而达到缓存 Promise 的效果
// 替换 service。目前只有 useCachePlugin 插件内部实现该方法,可实现缓存 Promise 的功能
let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);
// 其它插件走这个逻辑
if (!servicePromise) {
servicePromise = this.serviceRef.current(...params);
}
const res = await servicePromise;
onSuccess/onError/onFinally 请求结果
整个请求过程是包在 try catch 里面的
下文代码有个判断 currentCount !== this.count
,调用 runAsync
方法的时候默认 currentCount === this.count
,区分是否为正常流程的当前请求。当调用取消请求方法的时候,this.count += 1;
,此时两者不相等,直接返回空的 Promise
try {
// onRequest ...
// 区分是否为正常流程的当前请求(调用取消请求方法的时候 this.count 会加1)
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);
// 执行插件 onSuccess 的生命周期钩子
this.runPluginHandler('onSuccess', res, params);
// 执行外部传入的 onFinally 回调
this.options.onFinally?.(params, res, undefined);
// 执行插件 onFinally 的生命周期钩子
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);
// 执行插件 onError 的生命周期钩子
this.runPluginHandler('onError', error, params);
// 执行外部传入的 onFinally 回调
this.options.onFinally?.(params, undefined, error);
// 执行插件 onFinally 的生命周期钩子
if (currentCount === this.count) {
this.runPluginHandler('onFinally', params, undefined, error);
}
throw error;
}
cancel
取消请求
cancel() {
this.count += 1; // 标识加1,区分是否为正常流程的当前请求
this.setState({
loading: false,
});
// 执行插件 onCancel 的生命周期钩子
this.runPluginHandler('onCancel');
}
run
实现是直接调用 runAsync
方法,跟 runAsync
区别是 run
会自动 catch 异常且无返回值
run(...params: TParams) {
this.runAsync(...params).catch((error) => {
if (!this.options.onError) {
console.error(error);
}
});
}
refresh
使用上一次的 params,重新调用 run
refresh() {
// @ts-ignore
this.run(...(this.state.params || []));
}
refreshAsync
与 run 用法一致,但返回的是 Promise,需要自行处理异常
refreshAsync() {
// @ts-ignore
return this.runAsync(...(this.state.params || []));
}
mutate
直接修改 data,onMutate
这个钩子是唯一不算在请求的生命周期里
mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
// data 支持传入函数
const targetData = isFunction(data) ? data(this.state.data) : data;
// 执行插件 onMutate 的生命周期钩子
this.runPluginHandler('onMutate', targetData);
// 更新 state 中的 data
this.setState({
data: targetData,
});
}
插件功能实现
讲完 useRequest 的核心实现,接下来就来讲 useRequest 支持的多种功能是如何实现的。
Loading Delay
首先第一个是 Loading Delay
通过设置 options.loadingDelay
,可以延迟 loading
变 e 成 true
的时间,有效防止闪烁。
用法
假如 getUsername
在 300ms 内返回,则 loading
不会变成 true
,避免了页面展示 Loading...
的情况。
const { loading, data } = useRequest(getUsername, {
loadingDelay: 300
});
实现
该功能是通过 useLoadingDelayPlugin
插件实现,通过 onBefore 请求前设置 loading 状态来实现
暴露给外部使用的 loading
状态实际是 Fetch 实例维护的 state 状态。
async runAsync(...params: TParams): Promise<TData> {
const {
stopNow = false,
returnNow = false,
...state // 此时 useLoadingDelayPlugin 返回的 loading 为 false
} = this.runPluginHandler('onBefore', params);
// ...
this.setState({
loading: true,
params,
...state, // 这里覆盖 loading 状态,置为 false
});
}
useLoadingDelayPlugin
实现:
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();
// loadingDelay 时间范围内请求未完成,则才将 loading 状态设为 false
timerRef.current = setTimeout(() => {
fetchInstance.setState({
loading: true,
});
}, loadingDelay);
// loading 状态直接返回 false
return {
loading: false,
};
},
// 请求完成或取消清除定时器
onFinally: () => {
cancelTimeout();
},
onCancel: () => {
cancelTimeout();
},
};
};
轮询
通过设置 options.pollingInterval
,进入轮询模式,useRequest
会定时触发 service 执行。
用法
每隔 3000ms 请求一次 getUsername
。同时你可以通过 cancel
来停止轮询,通过 run/runAsync
来启动轮询,通过 options.pollingErrorRetryCount
轮询错误重试次数
const { data, run, cancel } = useRequest(getUsername, {
pollingInterval: 3000, // 轮询间隔,单位为毫秒
pollingErrorRetryCount: 3, // 轮询错误重试次数
});
实现
由 usePollingPlugin
插件实现。
实现原理:在 onFinally
生命周期里调用 refresh 方法,核心的几行代码如下:
// 每当完成一次请求,通过定时器在 `pollingInterval` ms 后再次发起请求
onFinally: () => {
// ...
timerRef.current = setTimeout(() => {
fetchInstance.refresh();
}, pollingInterval);
},
如何实现
pollingErrorRetryCount
(轮询错误重试次数。如果设置为 -1,则无限次)这个功能?
- 请求失败设置轮询次数。在
onError
生命周期钩子记录一个轮询错误次数countRef.current
,每次被调用就加 1。 - 请求成功重置轮询次数。在
onSuccess
钩子里重置countRef.current
为 0 - 轮询之前判断是否满足条件。
const countRef = useRef<number>(0);
// 判断轮询重试次数
if (
pollingErrorRetryCount === -1 ||
(pollingErrorRetryCount !== -1 && countRef.current <= pollingErrorRetryCount)
) {
// setTimeout 轮询
} else {
// 重置
countRef.current = 0;
}
如何实现
pollingWhenHidden
(在页面隐藏时,是否继续轮询。如果设置为 false,在页面隐藏时会暂时停止轮询,页面重新显示时继续上次轮询)功能?
- 判断页面是否可见:
isDocumentVisible
方法 - 主要针对
pollingWhenHidden
参数为 false 的情况,订阅页面可见状态的变化visibilitychange
- 每次一次新的请求需要在
onBefore
取消订阅
// 判断页面是否处于可见状态
function isDocumentVisible(): boolean {
if (isBrowser) {
return document.visibilityState !== 'hidden';
}
return true;
}
timerRef.current = setTimeout(() => {
// !pollingWhenHidden && 页面不可见
if (!pollingWhenHidden && !isDocumentVisible()) {
// 通过 subscribeReVisible 方法进行订阅监听页面可见状态的变化
unsubscribeRef.current = subscribeReVisible(() => {
fetchInstance.refresh();
});
} else {
fetchInstance.refresh();
}
}, pollingInterval);
来看看 subscribeReVisible
方法的实现,实际就类似于发布订阅:
// 事件监听队列
const listeners: Listener[] = [];
// 订阅事件
function subscribe(listener: Listener) {
// 将监听函数添加到事件队列
listeners.push(listener);
// 返回取消事件订阅的方法
return function unsubscribe() {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}
if (isBrowser) {
const revalidate = () => {
// 页面不可见时不处理
if (!isDocumentVisible()) return;
// 页面可见,循环执行事件监听队列的事件
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
};
// 监听 visibilitychange
window.addEventListener('visibilitychange', revalidate, false);
}
Ready
useRequest 提供了一个 options.ready
参数,当其值为 false 时,请求永远都不会发出。
其具体行为如下:
当 manual=false
自动请求模式时,每次 ready
从 false 变为 true 时,都会自动发起请求,会带上参数 options.defaultParams
。
当 manual=true
手动请求模式时,只要 ready=false
,则通过 run/runAsync
触发的请求都不会执行。
用法
手动模式下 ready 的行为。只有当 ready 等于 true 时,run 才会执行。
export default () => {
const [ready, { toggle }] = useToggle(false);
const { data, loading, run } = useRequest(getUsername, {
ready,
manual: true,
});
};
实现
const useAutoRunPlugin: Plugin<any, any[]> = (
fetchInstance,
{ manual, ready = true, defaultParams = [], refreshDeps = [], refreshDepsAction },
) => {
const hasAutoRun = useRef(false);
hasAutoRun.current = false;
// 只在依赖更新时执行
useUpdateEffect(() => {
// manual 为 false 表示自动请求模式
if (!manual && ready) {
hasAutoRun.current = true;
fetchInstance.run(...defaultParams); // 自动执行请求
}
}, [ready]);
return {
// 请求前的钩子
onBefore: () => {
// ready 为 false,返回 stopNow 为 true,表示请求不会发出
if (!ready) {
return {
stopNow: true,
};
}
},
};
};
在 Fetch 类中执行请求前:
const {
stopNow = false,
returnNow = false,
...state
} = this.runPluginHandler('onBefore', params);
// 为 true,则停止发送请求,返回空的 promise
if (stopNow) {
return new Promise(() => {});
}
依赖刷新
useRequest 提供了一个 options.refreshDeps
参数,当它的值变化后,会重新触发请求。
用法
const [userId, setUserId] = useState('1');
const { data, run } = useRequest(() => getUserSchool(userId), {
refreshDeps: [userId],
});
上面的示例代码,useRequest 会在初始化和 userId 变化时,触发函数执行。
与下面代码实现功能完全一致:
const [userId, setUserId] = useState('1');
const { data, refresh } = useRequest(() => getUserSchool(userId));
useEffect(() => {
refresh();
}, [userId]);
实现
这个也是由 useAutoRunPlugin
插件实现,主要就是监听 refreshDeps
依赖
const hasAutoRun = useRef(false);
hasAutoRun.current = false; // 每一次重新执行都重置为 false
useUpdateEffect(() => {
// manual 默认值为 false,ready 默认值为true
if (!manual && ready) {
hasAutoRun.current = true; // 请求前设置为 true
fetchInstance.run(...defaultParams); // 自动执行请求
}
}, [ready]);
useUpdateEffect(() => {
if (hasAutoRun.current) {
return;
}
if (!manual) {
hasAutoRun.current = true;
// 依赖变化的时候的回调,有传就执行
if (refreshDepsAction) {
refreshDepsAction();
} else {
fetchInstance.refresh(); // 重新发起请求
}
}
}, [...refreshDeps]);
屏幕聚焦重新请求
通过设置 options.refreshOnWindowFocus,在浏览器窗口 refocus 和 revisible 时,会重新发起请求。
用法
const { data } = useRequest(getUsername, {
refreshOnWindowFocus: true,
});
你可以点击浏览器外部,再点击当前页面来体验效果(或者隐藏当前页面,重新展示),如果和上一次请求间隔大于 5000ms,则会重新请求一次。Demo
实现
该功能由 useRefreshOnWindowFocusPlugin
插件实现。通过监听 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 {};
};
limit
实际就是节流函数
function limit(fn: any, timespan: number) {
let pending = false;
return (...args: any[]) => {
if (pending) return; // pending 阶段不进行请求,直接返回
pending = true;
fn(...args);
setTimeout(() => {
// 超过 timespan 时间范围,重置 pending
pending = false;
}, timespan);
};
}
接下来的关键就是这个 subscribeFocus
方法了,跟我们上面写的 subscribeReVisible
差不多
// 事件监听队列
const listeners: Listener[] = [];
// 订阅事件
function subscribe(listener: Listener) {
// 将监听函数添加到事件队列
listeners.push(listener);
// 返回取消事件订阅的方法
return function unsubscribe() {
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
};
}
if (isBrowser) {
const revalidate = () => {
// 页面不可见的时候和没网络的时候不处理
if (!isDocumentVisible() || !isOnline()) return;
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
};
// 监听 `visibilitychange` 和 `focus` 事件
window.addEventListener('visibilitychange', revalidate, false);
window.addEventListener('focus', revalidate, false);
}
防抖
通过设置 options.debounceWait
,进入防抖模式,此时如果频繁触发 run
或者 runAsync
,则会以防抖策略进行请求。
用法
const { data, run } = useRequest(getUsername, {
debounceWait: 300,
manual: true
});
如上示例代码,频繁触发 run,只会在最后一次触发结束后等待 300ms 执行。
实现
该功能是通过 useDebouncePlugin
插件实现的,实际底层调用的是 lodash/debounce
,所以也支持参数 options.debounceWait
、options.debounceLeading
、options.debounceTrailing
、options.debounceMaxWait
下面只放出关键代码:
useEffect(() => {
if (debounceWait) {
// 保存 runAsync 原方法
const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
// 基于lodash 的 debounce 封装,执行完的返回结果有 cancel 方法
debouncedRef.current = debounce(
(callback) => {
callback();
},
debounceWait,
options,
);
// debounce runAsync should be promise
// https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
fetchInstance.runAsync = (...args) => {
// 返回 Promise
return new Promise((resolve, reject) => {
debouncedRef.current?.(() => {
// 执行原函数
_originRunAsync(...args)
.then(resolve)
.catch(reject);
});
});
};
return () => {
// 取消函数调用
debouncedRef.current?.cancel();
// 还原成原来的 runAsync 函数
fetchInstance.runAsync = _originRunAsync;
};
}
}, [debounceWait, options]);
节流
通过设置 options.throttleWait
,进入节流模式,此时如果频繁触发 run
或者 runAsync
,则会以节流策略进行请求。
用法
const { data, run } = useRequest(getUsername, {
throttleWait: 300,
manual: true
});
如上示例代码,频繁触发 run,只会每隔 300ms 执行一次。
实现
该功能使用 useThrottlePlugin
实现,与 useDebouncePlugin
的思路一致,只不过换成了 lodash/throttle
去实现
useEffect(() => {
if (throttleWait) {
// 保存 runAsync 原方法
const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
// 基于lodash 的 throttle 封装,执行完的返回结果有 cancel 方法
throttledRef.current = throttle(
(callback) => {
callback();
},
throttleWait,
options,
);
// throttle runAsync should be promise
// https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
fetchInstance.runAsync = (...args) => {
// 返回 Promise
return new Promise((resolve, reject) => {
// 执行原函数
throttledRef.current?.(() => {
_originRunAsync(...args)
.then(resolve)
.catch(reject);
});
});
};
return () => {
// 还原成原来的 runAsync 函数
fetchInstance.runAsync = _originRunAsync;
// 取消函数调用
throttledRef.current?.cancel();
};
}
}, [throttleWait, throttleLeading, throttleTrailing]);
缓存 & SWR
- 设置了
options.cacheKey
,useRequest 会将当前请求成功的数据缓存起来。下次组件初始化时,如果有缓存数据,我们会优先返回缓存数据,然后在背后发送新请求,也就是 SWR 的能力。 - 通过
options.staleTime
设置数据保持新鲜时间,在该时间内,我们认为数据是新鲜的,不会重新发起请求。 - 通过
options.cacheTime
设置数据缓存时间,超过该时间,我们会清空该条缓存数据。
用法
const { data, loading, refresh } = useRequest(getArticle, {
cacheKey: 'cacheKey-share',
});
实现
缓存主要由 useCachePlugin
插件实现。在看主逻辑前,先看看涉及该插件有关的三个 utils 文件。
utils/cache.ts
先来看看缓存是如何管理的,ahooks 是抽到一个 packages/hooks/src/useRequest/src/utils/cache.ts
里面,暴露三个 utils 方法:getCache
、setCache
、clearCache
方法,通过 Map 数据结构来管理缓存数据
// 通过 Map 数据结构来缓存
const cache = new Map<CachedKey, RecordData>();
// 设置缓存
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) {
// 有设置超过缓存时间 cacheTime,则删除该条缓存数据
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 };
utils/cacheSubscribe.ts
实现了发布订阅,主要暴露两个方法:
subscribe
:订阅事件trigger
触发指定缓存 key 对应的所有事件
// 事件监听器
const listeners: Record<string, Listener[]> = {};
// 触发指定缓存 key 对应的所有事件
const trigger = (key: string, data: any) => {
if (listeners[key]) {
listeners[key].forEach((item) => item(data));
}
};
// 订阅事件
const subscribe = (key: string, listener: Listener) => {
// 一个 key 值可对应多个事件
if (!listeners[key]) {
listeners[key] = [];
}
// 添加到事件监听数组
listeners[key].push(listener);
// 返回清除订阅的方法
return function unsubscribe() {
const index = listeners[key].indexOf(listener);
listeners[key].splice(index, 1);
};
};
utils/cachePromise.ts
主要暴露两个方法:
getCachePromise
:获取缓存 promisesetCachePromise
:设置 promise 缓存
代码概览
- 插件初始化时先尝试获取缓存,有缓存且没过期则使用缓存的 data 和 params 进行替换,订阅同一 cacheKey 更新
- 实现住逻辑用到了钩子函数:
onBefore
、onRequest
、onSuccess
、onMutate
const useCachePlugin: Plugin<any, any[]> = (
fetchInstance,
{
cacheKey, // 请求唯一标识
cacheTime = 5 * 60 * 1000, // 缓存数据回收时间
staleTime = 0, // 缓存数据保持新鲜时间
setCache: customSetCache, // 自定义设置缓存
getCache: customGetCache, // 自定义读取缓存
},
) => {
const unSubscribeRef = useRef<() => void>();
const currentPromiseRef = useRef<Promise<any>>();
// 设置缓存
const _setCache = (key: string, cachedData: CachedData) => {
// 有传入自定义设置缓存则优先使用自定义方法
if (customSetCache) {
customSetCache(cachedData);
} else {
cache.setCache(key, cacheTime, cachedData);
}
// 触发指定缓存 key 对应的所有事件。key 值相同的话 data 数据是共享的
cacheSubscribe.trigger(key, cachedData.data);
};
// 读取缓存
const _getCache = (key: string, params: any[] = []) => {
// 有传入自定义读取缓存则优先使用自定义方法
if (customGetCache) {
return customGetCache(params);
}
return cache.getCache(key);
};
useCreation(() => {
if (!cacheKey) {
return;
}
// 初始化时尝试从缓存获取数据
const cacheData = _getCache(cacheKey);
// 有缓存
if (cacheData && Object.hasOwnProperty.call(cacheData, 'data')) {
// 使用缓存的 data 和 params 替换
fetchInstance.state.data = cacheData.data;
fetchInstance.state.params = cacheData.params;
// staleTime 为-1表示数据永远新鲜 或 当前时间还在新鲜时间范围内
if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
// 直接使用缓存的话无需 loading
fetchInstance.state.loading = false;
}
}
// 订阅 cacheKey 更新(同个 cacheKey 缓存数据相同)
unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (data) => {
fetchInstance.setState({ data });
});
}, []);
// 页面卸载时取消订阅
useUnmount(() => {
unSubscribeRef.current?.();
});
// 没有设置 cacheKey 则直接返回空对象
if (!cacheKey) {
return {};
}
// 核心逻辑
return {
onBefore: (params) => {},
onRequest: (service, args) => {},
onSuccess: (data, params) => {},
onMutate: (data) => {}
}
}
onBefore 阶段
主要逻辑:判断是否有缓存数据,有则返回数据,关键是 returnNow: true
;没有也返回数据,但不会阻塞请求进行
returnNow
参数在 runAsync
方法会用到:
async runAsync(...params: TParams): Promise<TData> {
const {
returnNow = false,
} = this.runPluginHandler('onBefore', params);
// 返回停止请求
if (returnNow) {
return Promise.resolve(state.data);
}
}
useCachePlugin
的 onBefore
实现:
onBefore: (params) => {
// 读取 cacheKey 缓存
const cacheData = _getCache(cacheKey, params);
// 无缓存数据
if (!cacheData || !Object.hasOwnProperty.call(cacheData, 'data')) {
return {};
}
// staleTime 为-1表示数据永远新鲜 或 当前时间还在新鲜时间范围内
if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
return {
loading: false,
data: cacheData?.data, // 缓存数据
error: undefined,
returnNow: true, // 标记立即返回
};
} else {
// 数据不新鲜,需要重新请求
// If the data is stale, return data, and request continue
return {
data: cacheData?.data,
error: undefined,
};
}
},
onRequest 阶段
主要逻辑:缓存 promise。通过记录本地请求 promise 比对的方式保证同一时间同个 cacheKey 的请求只能发起一个
onRequest: (service, args) => {
// 获取 promise 缓存
let servicePromise = cachePromise.getCachePromise(cacheKey);
// 有缓存 promise && 不等于上一次触发的请求,保证统
if (servicePromise && servicePromise !== currentPromiseRef.current) {
// 返回 servicePromise
return { servicePromise };
}
servicePromise = service(...args);
// 保存当前触发的 promise
currentPromiseRef.current = servicePromise;
// 设置 promise 缓存
cachePromise.setCachePromise(cacheKey, servicePromise);
return { servicePromise };
},
onSuccess & onMutate 阶段
这两个钩子里面做的事情是一样的,分别是:
- 先取消订阅
- 再
_setCache
更新缓存值 - 最后重新订阅 cacheKey
onSuccess: (data, params) => {
// 有缓存 cacheKey
if (cacheKey) {
// 先取消订阅
unSubscribeRef.current?.();
// 更新缓存值
_setCache(cacheKey, {
data,
params,
time: new Date().getTime(),
});
// 重新订阅
unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (d) => {
fetchInstance.setState({ data: d });
});
}
},
错误重试
该功能是通过 useRetryPlugin
插件实现的。
- 通过设置
options.retryCount
,指定错误重试次数,则 useRequest 在失败后会进行重试。 - 还可以设置
options.retryInterval
指定重试时间间隔。
用法
const { data, run } = useRequest(getUsername, {
retryCount: 3,
});
如上示例代码,在请求异常后,会做 3 次重试。
实现
核心实现:在 onError
生命周期钩子计数错误次数,然后通过定时器重试。
onError: () => {
countRef.current += 1; // 累计错误次数
// retryCount 为 -1 或 重试次数小于等于 retryCount,则执行
if (retryCount === -1 || countRef.current <= retryCount) {
// 如果不设置 retryInterval,默认采用简易的指数退避算法,取 1000 * 2 ** retryCount,也就是第一次重试等待 2s,第二次重试等待 4s,以此类推,如果大于 30s,则取 30s
const timeout = retryInterval ?? Math.min(1000 * 2 ** countRef.current, 30000);
timerRef.current = setTimeout(() => {
triggerByRetry.current = true; // 标记触发错误重试
fetchInstance.refresh(); // 重新请求
}, timeout);
} else {
countRef.current = 0;
}
},
结语
useRequest 的源码写到这了,文章比较长,有些不是特别关键的逻辑会遗漏,各位有兴趣的可以自己去看看完整源码。
原文链接:https://juejin.cn/post/7244818537776709687 作者:JackySummer