一、useRequest 介绍
useRequest 是一个强大的异步数据管理的 Hooks。
useRequest 通过插件式组织代码,核心代码极其简单,并且可以很方便的扩展出更高级的功能。目前已有能力包括:
- 自动请求/手动请求
- 轮询
- 防抖
- 节流
- 屏幕聚焦重新请求
- 错误重试
- loading delay
- SWR(stale-while-revalidate)
- 缓存
二、源码解析
1、入口文件useRequest.ts
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>[]);
}
useRequest入口函数非常简单,返回了useRequestImplement的执行结果。
useRequestImplement是useRequest的具体实现,在useRequest参数的基础上,加入了一些默认的插件。
2、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,
};
const serviceRef = useLatest(service);
const update = useUpdate();
// 用useCreation生成实例,避免重复新建实例,提升性能
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;
// 执行所有的插件
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 || [];
// @ts-ignore
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)),
} as Result<TData, TParams>;
}
useRequestImplement代码也是很简短,主要做了几件事情:
- 通过
useCreation来生成Fetch的请求实例,useCreation的作用是避免重复新建实例,提升性能。 - 执行插件里的
onInit钩子,并生成初始化的参数。 - 执行所有的插件。
- 执行自动请求。
- 组件卸载时取消请求实例。
- 返回调用
useRequest的参数。
3、Fetch.ts
Fetch的代码较长,我们拆开来一点点看。
3.1 constructor
主要是合并一些初始状态,包括插件执行onInit时返回的状态。
3.2 runAsync
runAsync是手动请求时返回的异步请求方法,后面可以接then和catch,是Fetch里的主要逻辑。
async runAsync(...params: TParams): Promise<TData> {
// 用来计数,当count和currentCount不一致时,中断请求
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);
}
// 执行useRequest参数里的onBefore回调
this.options.onBefore?.(params);
try {
// 执行onRequest钩子,返回promise
let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);
// 如果上面没有返回promise,则执行请求服务,返回一个promise
if (!servicePromise) {
servicePromise = this.serviceRef.current(...params);
}
// 执行服务的promise
const res = await servicePromise;
// 当请求取消时,count会不等,停止请求
if (currentCount !== this.count) {
// prevent run.then when request is canceled
return new Promise(() => {});
}
this.setState({
data: res,
error: undefined,
loading: false,
});
// 执行onSuccess和onFinally钩子
this.options.onSuccess?.(res, params);
this.runPluginHandler('onSuccess', res, params);
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和onFinally钩子
this.options.onError?.(error, params);
this.runPluginHandler('onError', error, params);
this.options.onFinally?.(params, undefined, error);
if (currentCount === this.count) {
this.runPluginHandler('onFinally', params, undefined, error);
}
throw error;
}
runAsync代码虽然多,但是只是做了几件事情:
- 执行请求服务。
- 当请求取消时,退出请求。
- 执行
onBefore、onRequest、onSuccess、onError、onFinally钩子。 - 返回成功和失败的
promise。
3.3 run
run在runAsync的基础上,捕获打印了错误,不再返回promise。
run(...params: TParams) {
this.runAsync(...params).catch((error) => {
if (!this.options.onError) {
console.error(error);
}
});
}
3.4 cancel
cancel方法很简单,就是让count+1,使它不等于当前的计数,使loading变为false,然后执行插件的onCancel取消钩子
cancel() {
this.count += 1;
this.setState({
loading: false,
});
this.runPluginHandler('onCancel');
}
3.5 mutate
mutate是手动更改返回参数,使页面更早地响应更改结果,而不必等接口响应。
mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
let targetData: TData | undefined;
if (isFunction(data)) {
// @ts-ignore
targetData = data(this.state.data);
} else {
targetData = data;
}
this.runPluginHandler('onMutate', targetData);
this.setState({
data: targetData,
});
}
做的事情很简单,如果传入参数为函数,先执行函数获取返回参数,然后把数据设置给useRequest的返回值data即可。另外还执行了插件的onMutate钩子。
3.5 其它方法
其它的方法都比较简单,不再一一解析源码,他们分别是:
- setState:设置状态,并且更新。
- runPluginHandler:执行插件的指定钩子。
- refresh:使用原来的参数重新请求。
- refreshAsync:使用原来的参数重新请求,返回异步promise。
三、插件机制
插件就是在某些生命周期执行某些逻辑的方法。
useRequest封装了许多默认的插件,同时还支持用户自定义插件。他们在useRequest创建实例后执行。
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
插件执行后返回一个对象,这个对象上挂载了一些特定生命周期执行的方法,在合适的时机时调用他们。
return {
onBefore: () => {},
onSuccess: () => {},
...
}
this.runPluginHandler('onSuccess', res, params);
可以看到,插件的生命周期钩子是通过封装的runPluginHandler方法调用的。
runPluginHandler方法很简单,就是执行所有插件的该生命周期。
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);
}