在日常开发工作中经常用到useRequest,但却不清楚内部的实现原理,这次就从源码层面深入分析 useRequest的实现与设计。
支持的功能
- 延迟loading
- 轮询
- 依赖刷新
- 屏幕聚焦重新请求
- 防抖
- 节流
- 缓存&SWR
- 错误重试
useRequest的源码结构
useRequest的核心代码位于ahooks项目的packages/hooks/src/useRequest/目录下,主要包含以下几个文件:
useRequest.ts:useRequest的入口文件,内置了一些默认的插件。useRequestImplement.ts:useRequest的实现文件,主要是导出各个方法,具体实现是通过new Fetch创建fetchInstance实例。Fetch.ts:核心内容,Fetch类的定义文件,该类负责实际发起请求和管理请求状态。plugins目录:包含各种插件的实现,用于扩展useRequest的功能。
useRequest的工作流程
1. 初始化与实例化
当在组件中调用useRequest时,实际上是在调用useRequestImplement函数。useRequestImplement首先会创建并初始化一个Fetch实例(通常命名为fetchInstance),这个实例将负责后续的所有请求操作。
2. 配置与插件
useRequest支持丰富的配置选项,如manual、retryDelay等,这些配置项通过options参数传入useRequestImplement。此外,useRequest还支持插件机制,允许开发者通过传入插件数组来扩展功能。这些插件在useRequestImplement内部被注册到fetchInstance上,并在请求的不同生命周期内(如请求前、请求成功、请求失败等)执行相应的回调。V3版本内置了以下八个插件:
- useDebouncePlugin:防抖
- useLoadingDelayPlugin:延迟loading
- usePollingPlugin:轮询
- useRefreshOnWindowFocusPlugin:屏幕聚焦重新请求
- useThrottlePlugin:节流
- useAutoRunPlugin:依赖刷新
- useCachePlugin:缓存
- useRetryPlugin:错误重试
3. 请求执行
请求的执行通过调用fetchInstance的run或runAsync方法来完成。这些方法内部会按照预设的流程(如onBefore、onRequest、onSuccess、onError、onFinally)执行相应的插件回调,并最终发起HTTP请求。请求的状态(如loading、data、error)会被实时更新并暴露给组件。
import { isFunction } from '../../utils';
import type { MutableRefObject } from 'react';
import type { FetchState, Options, PluginReturn, Service, Subscribe } from './types';
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: !options.manual,
...initState,
};
}
setState(s: Partial<FetchState<TData, TParams>> = {}) {
this.state = {
...this.state,
...s,
};
this.subscribe();
}
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);
}
async runAsync(...params: TParams): Promise<TData> {
this.count += 1;
const currentCount = this.count;
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);
}
this.options.onBefore?.(params);
try {
// replace service
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(() => {});
}
// const formattedResult = this.options.formatResultRef.current ? this.options.formatResultRef.current(res) : res;
this.setState({
data: res,
error: undefined,
loading: false,
});
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,
});
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;
}
}
run(...params: TParams) {
this.runAsync(...params).catch((error) => {
if (!this.options.onError) {
console.error(error);
}
});
}
cancel() {
……
}
refresh() {
……
}
refreshAsync() {
// @ts-ignore
return this.runAsync(...(this.state.params || []));
}
mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
const targetData = isFunction(data) ? data(this.state.data) : data;
this.runPluginHandler('onMutate', targetData);
this.setState({
data: targetData,
});
}
}
4. 请求取消与刷新
useRequest提供了cancel和refresh方法来分别取消当前请求和重新发起请求。cancel方法通过修改内部计数器来实现请求的取消,而refresh方法则相当于重新调用run方法。
cancel() {
this.count += 1;
this.setState({
loading: false,
});
this.runPluginHandler('onCancel');
}
refresh() {
// @ts-ignore
this.run(...(this.state.params || []));
}
Fetch类的核心方法
Fetch类是useRequest实现的核心,它封装了请求的逻辑和状态管理。以下是几个关键的方法:
constructor:构造函数,负责初始化Fetch实例的状态和配置。runAsync:异步执行请求的方法,按照预设的流程执行插件回调,并处理请求结果。cancel:取消当前请求的方法,通过修改内部计数器来实现。refresh:重新发起请求的方法,内部调用runAsync。
插件机制
useRequest的插件机制是其灵活性的重要体现。每个插件都是一个对象,包含一系列生命周期钩子(如onBefore、onRequest、onSuccess等),这些钩子在请求的不同阶段被调用。通过插件,开发者可以轻松地扩展useRequest的功能,如添加请求缓存、自动重试、轮询等。虽然文档上没有自定义插件的相关说明,但从源码的上看,useRequest的第三个参数是可以接受插件数组的,demo如下:
import { useRequest } from "ahooks";
const service = (parmas) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('parmas', parmas)
resolve('响应成功!!')
}, 1000)
})
}
const cutomPlugins = (fetchInstance, fetchOptions) => {
// 自定义的实现逻辑
return {
onMutate (a,b,c) {
console.log('onMutate', a,b,c)
},
onBefore (a,b,c) {
console.log('onBefore', a,b,c)
},
onRequest (a,b,c) {
console.log('onRequest', a,b,c)
},
onSuccess (a,b,c) {
console.log('onSuccess', a,b,c)
},
onFinally (a,b,c) {
console.log('onFinally', a,b,c)
},
onError (a,b,c) {
console.log('onError', a,b,c)
},
onCancel (a,b,c) {
console.log('onCancel', a,b,c)
},
}
}
const UseRequestDemo = () => {
const res = useRequest(service, {
manual: false,
defaultParams: [{
name: 'candy',
age: 8
}]
}, [cutomPlugins]);
console.log("res", res);
return <div>useRequestDemo</div>;
};
export default UseRequestDemo;
总结
useRequest通过其强大的功能和灵活的插件机制,为React开发中的异步请求处理提供了完美的解决方案。从源码层面深入分析useRequest,我们不仅可以看到其背后的巧妙的设计,还能更好地理解其工作原理和内部机制。