源码地址:github.com/alibaba/hoo...
前言
我们一看到 request 的字样自然就联想到 useRequest 一定是调后端接口的请求库,其实并不是。
它是一个异步数据管理的 Hooks,也就是说它是用来给异步方法丰富更多强大的功能的,它本身和远程数据请求库是解耦的,它可以配合 axios
、原生fetch
、甚至只是一个简单的 Promise
都是 OK 的。
个人觉得通过阅读 useRequest
源码来入门学习 js 的插件和生命周期设计非常合适,源码本身也比较简单
源码解析
入口文件:
ts
import useRequest from './src/useRequest';
import { clearCache } from './src/utils/cache';
export { clearCache };
export default useRequest;
useRequest
对外提供了两个方法,一个是方法本身,一个是 clearCache
,它可以让使用者自行清除已缓存的请求数据
useRequst.ts
ts
import useAutoRunPlugin from './plugins/useAutoRunPlugin';
import useCachePlugin from './plugins/useCachePlugin';
import useDebouncePlugin from './plugins/useDebouncePlugin';
import useLoadingDelayPlugin from './plugins/useLoadingDelayPlugin';
import usePollingPlugin from './plugins/usePollingPlugin';
import useRefreshOnWindowFocusPlugin from './plugins/useRefreshOnWindowFocusPlugin';
import useRetryPlugin from './plugins/useRetryPlugin';
import useThrottlePlugin from './plugins/useThrottlePlugin';
import type { Options, Plugin, Service } from './types';
import useRequestImplement from './useRequestImplement';
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>[]);
}
export default useRequest;
可以看出,整体 useRequest
可以分成三部分:
useRequestImplement
:用来生成最核心的 Fetch 方法及运行插件和生命周期plugins
:以插件的形式实现各种功能service
:外部传入的异步操作方法
useRequestImplement
ts
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 };
// useLatest hooks 可以获取到的 service 的最新值,避免闭包问题
const serviceRef = useLatest(service);
// 调用 update 方法可以强制刷新 react 组件
const update = useUpdate();
// useCreation 类似 useMemo
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(() => {
// 如果 manual 为 false,则直接执行 run 方法,发起异步操作
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();
});
// 调用 useRequest 返回的方法对象
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>;
}
掌握几个核心点:
fetchInstance
为异步操作的核心对象,它返回了Fetch
类的实例化对象(这个 Fetch 不是那个浏览器原生 Fetch 方法),并接受四个参数
- serviceRef:异步操作方法
- fetchOptions:请求的配置参数
- update:强制更新 react 组件方法
- initState:初始状态对象 → 通过执行每个 plugin 的
onInit
方法收集整合得到
- 执行每个 plugin 方法,传入
fetchInstance
和fetchOptions
,将执行结果赋值给fetchInstance.pluginImpls
- 在组件首次渲染(useMount)时,如果没有设置
manual
,则直接执行fetchInstance.run
方法执行异步操作 - 在组件卸载(useUnmount)时,执行
fetchInstance.cancel
方法
Fetch
Fetch 是核心类,这里分成几个阶段来解析:
初始化
ts
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: !options.manual,
...initState,
};
}
}
这里初始化了三个变量:
pluginImpls
:保存插件的回调数据对象count
:计数器,具体用法后面会讲state
:内部状态,初始化有 loading、params、data 和 error
同时接受四个参数,serviceRef
/ options
/ subscribe
/ initState
,具体含义上面 fetchInstance
里有讲到,不再赘述。
执行
无论 manual
是否设置,最终都以执行 run
方法为开始,其内部调用了 runAsync
方法传入请求参数 params
ts
run(...params: TParams) {
this.runAsync(...params).catch((error) => {
if (!this.options.onError) {
console.error(error);
}
});
}
下面的 runAsync
的源码部分,其中省略了插件逻辑和生命周期,后面会单独讲
ts
async runAsync(...params: TParams): Promise<TData> {
this.count += 1;
const currentCount = this.count;
this.setState({
loading: true,
params
});
try {
const res = await this.serviceRef.current(...params);
if (currentCount !== this.count) {
// prevent run.then when request is canceled
return new Promise(() => {});
}
this.setState({
data: res,
error: undefined,
loading: false,
});
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,
});
if (currentCount === this.count) {
this.runPluginHandler('onFinally', params, undefined, error);
}
throw error;
}
}
讲 runAsync 之前,我们先看下里面用到的一些方法都干了啥
ts
// 设置 state,同时调用 subscribe 方法,触发视图组件更新
setState(s: Partial<FetchState<TData, TParams>> = {}) {
this.state = {
...this.state,
...s,
};
// 这里的 subscribe 就是调用 Fetch 时传入的 update 方法
this.subscribe();
}
ts
// 执行所有插件中的 event 事件并收集返回对象,整合后输出
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);
return Object.assign({}, ...r);
}
还有 this.count
和 currentCount
的逻辑,我们看下 cancel
方法源码:
ts
cancel() {
this.count += 1;
this.setState({
loading: false,
});
this.runPluginHandler('onCancel');
}
当调用 cancel 方法时,this.count 会加 1。这样this.count
和 currentCount
不再相等。
再回过头来看 runAsync
的逻辑就很清晰了
- 设置 loading 和 params,触发视图组件更新
- 执行
serviceRef
发出异步请求,接受返回结果 - 对比
this.count
和currentCount
,如果不相等直接返回空 promise,不再执行后续逻辑,这也就实现了一个取消请求的操作,不过要注意的是真实请求依然会继续进行 - 设置 loading、data、error,执行
onFinally
插件事件回调
Fetch 里面还有一些其他方法,比如 refresh
、refreshAsync
、mutate
比较简单,看下源码就能理解。
小结
到此,useRequest 的核心逻辑就读完了,整体分为数据初始化和发送请求两部分。useRequestImplement
通过 hooks 把 react 视图 和 异步数据处理逻辑链接在一起。
下面我们再单独来看 useRequest 的插件和事件订阅是如何实现的。
插件
ts
useRequestImplement(service, options, [
...(plugins || []),
useDebouncePlugin,
useLoadingDelayPlugin,
usePollingPlugin,
useRefreshOnWindowFocusPlugin,
useThrottlePlugin,
useAutoRunPlugin,
useCachePlugin,
useRetryPlugin,
]
我们可以看到,useRequest 以插件形式来实现各种能力,比如防抖、延迟 loading 等等,好处是使核心逻辑更简洁、各功能之间相互解耦可以任意组合。
功能实现
传入到 useRequestImplement 的插件方法会被依次执行,每个插件都会接受到 fetchInstance
和 fetchOptions
,可以返回一个对象。
ts
// useRequestImplement.ts
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
插件对象还可以通过 onInit
方法设置初始化 state
ts
const fetchInstance = useCreation(() => {
const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
return new Fetch<TData, TParams>(
serviceRef,
fetchOptions,
update,
Object.assign({}, ...initState),
);
}, []);
在 Fetch
中,有两个触发生命周期的方法:
一个是使用者通过 options
参数传入的回调函数,比如
ts
const { loading, run } = useRequest(editUsername, {
onBefore: (params) => {
message.info(`Start Request: ${params[0]}`);
},
onSuccess: (result, params) => {
message.success(`The username was changed to "${params[0]}" !`);
},
onError: (error) => {
message.error(error.message);
},
onFinally: (params, result, error) => {
message.info(`Request finish`);
},
});
在 Fetch
中,在发送异步操作的各个节点执行回调
ts
class Fetch<TData, TParams extends any[]> {
async runAsync(...params: TParams): Promise<TData> {
// ...
// ✅ 执行 onBefore 回调
this.options.onBefore?.(params);
try {
const res = await this.serviceRef.current(...params);
// ✅ 执行 onSuccess 回调
this.options.onSuccess?.(res, params);
// ✅ 执行 onFinally 回调
this.options.onFinally?.(params, res, undefined);
return res;
} catch (error) {
// ✅ 执行 onError 回调
this.options.onError?.(error, params);
this.options.onFinally?.(params, undefined, error);
throw error;
}
}
}
另一个则是插件方法执行后返回的事件订阅,下面是一个插件的实例代码
ts
const useAutoRunPlugin: Plugin<any, any[]> = (
fetchInstance,
fetchOptions,
) => {
useEffect(() => {
// ...
}, []);
return {
onBefore: () => {
// ...
},
onRequest: () => {},
onSuccess: () => {}
};
};
在 Fetch
源码中,通过 runPluginHandler
方法来执行插件返回的执行 event,看源码:
ts
class Fetch<TData, TParams extends any[]> {
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);
return Object.assign({}, ...r);
}
async runAsync(...params: TParams): Promise<TData> {
this.count += 1;
const currentCount = this.count;
// ✅ 执行 onBefore 回调
const {
stopNow = false,
returnNow = false,
...state
} = this.runPluginHandler('onBefore', params);
// stop request
if (stopNow) {
return new Promise(() => {});
}
this.setState({
loading: true,
params,
...state,
});
// return now
if (returnNow) {
return Promise.resolve(state.data);
}
try {
// replace service
// ✅ 执行 onRequest 回调
let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);
if (!servicePromise) {
servicePromise = this.serviceRef.current(...params);
}
const res = await servicePromise;
if (currentCount !== this.count) {
// prevent run.then when request is canceled
return new Promise(() => {});
}
this.setState({
data: res,
error: undefined,
loading: false,
});
// ✅ 执行 onSuccess 回调
this.runPluginHandler('onSuccess', res, params);
if (currentCount === this.count) {
// ✅ 执行 onFinally 回调
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.runPluginHandler('onError', error, params);
if (currentCount === this.count) {
this.runPluginHandler('onFinally', params, undefined, error);
}
throw error;
}
}
}
我们可以看到,插件的生命周期事件不仅是获取信息,还可以给 Fetch
回传参数,来控制核心功能里的一些逻辑。
我们找一个插件看下具体逻辑,比如 useAutoRunPlugin.ts
这个插件实现的功能是:
通过设置 options.ready
,可以控制请求是否发出。当其值为 false
时,请求永远都不会发出。
其具体行为如下:
- 当
manual=false
自动请求模式时,每次ready
从false
变为true
时,都会自动发起请求,会带上参数options.defaultParams
。 - 当
manual=true
手动请求模式时,只要ready=false
,则通过run/runAsync
触发的请求都不会执行。
ts
// useAutoRunPlugin.ts
import { useRef } from 'react';
import useUpdateEffect from '../../../useUpdateEffect';
import type { Plugin } from '../types';
const useAutoRunPlugin: Plugin<any, any[]> = (
fetchInstance,
// 使用者传入的 options
{ manual, ready = true, defaultParams = [] },
) => {
// 插件内部可以使用 react hooks
useUpdateEffect(() => {
if (!manual && ready) {
fetchInstance.run(...defaultParams);
}
}, [ready]);
return {
// 注册生命周期回调
onBefore: () => {
if (!ready) {
return {
stopNow: true,
};
}
},
};
};
useAutoRunPlugin.onInit = ({ ready = true, manual }) => {
return {
loading: !manual && ready,
};
};
export default useAutoRunPlugin;
它有几个核心思路:
- 因为
useRequest
是一个自定义 hooks,因此这些插件也是自定义 hooks,可以使用 hooks 实现功能 - 依赖
ready
的变化,为true
时,调用fetchInstance.run
方法执行异步操作 - 通过在
onBefore
生命周期中返回stopNow
来中断请求,我们看下Fetch
的逻辑,在runAsync
中获取到stopNow
为true
时直接返回了空的Promise
ts
async runAsync(...params: TParams): Promise<TData> {
const {
stopNow = false,
returnNow = false,
...state
} = this.runPluginHandler('onBefore', params);
// stop request
if (stopNow) {
return new Promise(() => {});
}
}
- 通过
useAutoRunPlugin.onInit
方法设置state.loading
对象
小结
其他的插件的实现思路大同小异,都是使用 React hooks 和生命周期函数来实现功能。
大家是否有发现,useRequest
的第三个入参是插件数组,外部可以传入自定义插件,但在使用文档中并没有写,我猜作者是不想让我们传入自定义插件,从源码上看,它的插件实现并没有实现核心与插件的完全解耦,比如 useAutoRunPlugin
返回的 stopNow
参数,在 Fetch
中直接使用了,如果 useAutoRunPlugin
有修改,那 Fetch
核心逻辑也要改动。总不能让大家传入自定义插件的同时,还要提个 PR 去改 Fetch
吧,不过对于 useRequest
来说,这种简单的插件设计也够用了。
最后,通过阅读 useRequest
源码,我们对生命周期和插件式编程思想应该都有了入门级的理解,对今后实现自身业务也会有一定帮助。