前言
我们日常开发 React 项目时,不可避免的要发送很多请求,那如何提炼出一个满足大部分场景,并且又能够优雅使用的 Hooks
来提高我们开发的效率呢,接下来我们来通过源码看一下目前应用很广泛的 ahooks 的 useRequest 是如何做的。
useRequest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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 的执行结果 这里比较特殊的是第三个参数,它是一个插件数组。
useRequest 提供了 Loading Delay、轮询、Ready 等等一系列功能,那这些功能其实都对应着一个插件,这样通过插件的机制很大程度上降低了各个功能之间的耦合度。
另外我们需要了解下这些插件执行后返回了什么,这个后面会用到。
1 2 3 4 5 6 7 8 9 10 11 12 13
| const useCachePlugin: Plugin<any, any[]> = () => { return { onBefore: (params) => { }, onRequest: (service, args) => { }, onSuccess: (data, params) => { }, onMutate: (data) => { }, }; };
|
这里用 useCachePlugin 插件做例子,可以看到他的返回值是一些回调钩子,其实他的意思就是在对应的钩子执行对应的方法。
接下来我们看下 useRequestImplement 是如何实现的。
useRequestImplement
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| 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(() => { }, []); fetchInstance.options = fetchOptions; fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions)); useMount(() => { }); useUnmount(() => { }); return { loading, data, error, params, cancel, refresh, refreshAsync, run, runAsync, mutate, }; }
|
首先看参数
service 是真正要请求的方法,
options 是配置项
plugins 是插件数组
再看内部定义的一些变量和方法
manual 是作为是否默认执行 useRequest 的依据,为 true 时,不会默认执行,需要手动触发
fetchOptions 是将配置项整合
serviceRef 返回当前最新的 service
update 可以理解为 forceUpdate, 让组件重新渲染
这里最重要的就是 fetchInstance ,返回的所有值都要基于 fetchInstance 。
useMount / useUnmount
1 2 3 4 5 6 7 8 9 10 11
| useMount(() => { if (!manual) { const params = fetchInstance.state.params || options.defaultParams || []; fetchInstance.run(...params); } });
useUnmount(() => { fetchInstance.cancel(); });
|
这里看到hooks的名字应该也能猜到,这是两个生命周期,在组件初始化时,如果设置了 manual 为 true,就执行请求, 在组件销毁时,取消请求
fetchInstance
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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;
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
|
fetchInstance 是一个通过 useCreation 创建出来的一个常量对象,这个对象是通过 new Fetch
创建的。然后将配置项和插件的执行结果赋值给对象的属性。fetchInstance 的一系列属性方法都是来自这个 Fetch。
Fetch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| 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>> = {}) { }
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) { }
async runAsync(...params: TParams): Promise<TData> { }
run(...params: TParams) { }
cancel() { }
refresh() { }
refreshAsync() { }
mutate(data?: TData | ((oldData?: TData) => TData | undefined)) { } }
|
首先在创建实例时,传入了四个参数,从 fetchInstance 中创建实例的时候可以看到这四个值分别对应什么,
- serviceRef 就是要请求的 service
- options 是配置项
- subscribe 是让组件重新渲染
- initState 是初始化的 state 值
然后在 constructor 中初始化了 state。
剩下的除了 runPluginHandler 是内部使用的方法,其他都是在 useRequestImplement 抛出提供给我们使用的方法。
最重要的两个方法应该是 runPluginHandler 和 runAsync。
setState
1 2 3 4 5 6 7 8 9 10 11
| setState(s : Partial<FetchState<TData, TParams>> = {} ) { this.state = { ...this.state, ...s, }; this.subscribe(); }
|
这个方法没什么好说的,就是更新 state,然后触发重新渲染。
runPluginHandler
1 2 3 4 5 6 7 8 9 10 11 12
| runPluginHandler(event : keyof PluginReturn<TData, TParams>, ... rest: any[] ) { const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean); return Object.assign({}, ...r); }
|
我们在开始的时候有介绍给插件的返回值是什么,runPluginHandler 就是在特定的阶段执行对应的插件方法,event 就是代表要执行的阶段。
我们来看一下 PluginReturn 类型,了解一下到底有多少个阶段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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
runAsync 就是真正去执行请求的方法,接下来我们根据不同的阶段来分别说明是如何处理的
onBefore
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const { stopNow = false, returnNow = false, ...state } = this.runPluginHandler('onBefore', params);
if (stopNow) { return new Promise(() => { }); }
this.setState({ loading: true, params, ...state, });
if (returnNow) { return Promise.resolve(state.data); }
this.options.onBefore?.(params);
|
onBefore 阶段可以理解为在发起真正请求之前做的事情,首先说一下 stopNow、returnNow 分别代表了什么
- stopNow:ready(可以在option中配置,默认为true) 为 false 时,stopNow 为 true,不会发出请求,直接返回
- returnNow:该值在开启缓存时,并且缓存没有过期时为 true,这样就会把缓存的值返回
OnBefore 执行流程就是 判读是否需要请求 -> 更新 state -> 是否缓存可用 -> 自定义 onBefore 函数
onRequest
1 2 3 4 5 6 7 8
| let {servicePromise} = this.runPluginHandler('onRequest', this.serviceRef.current, params);
if (!servicePromise) { servicePromise = this.serviceRef.current(...params); }
const res = await servicePromise;
|
onRequest 是真正发起请求的阶段
servicePromise 就是请求方法,因为只有缓存插件 useCachePlugin 有 onRequest 阶段,所以初始化 servicePromise 时首先判断是否缓存能用,
如果没有缓存,就执行请求方法
onSuccess
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| if (currentCount !== this.count) { return new Promise(() => { }); }
this.setState({ data: res, error: undefined, loading: false, });
this.options.onSuccess?.(res, params); this.runPluginHandler('onSuccess', res, params);
|
onSuccess 阶段就是请求成功后的阶段,我们会发现很多地方用到了 currentCount !== this.count 这个判断,
这个判断实际上就是用来校验当前的 runAsync 是否被打断了,如果在执行 runAsync 的时候,我们手动通过 cancel
去取消,那么就会通过这个校验,结束本次执行。
onFinally
1 2 3 4 5 6 7
| this.options.onFinally?.(params, res, undefined);
if (currentCount === this.count) { this.runPluginHandler('onFinally', params, res, undefined); }
return res;
|
onFinally 阶段就是最后触发的一个阶段,执行一下我们自定义的 onFinally。
onError 错误处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| if (currentCount !== this.count) { 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
1 2 3 4 5 6 7 8 9 10 11
| run(...params : TParams ) { this.runAsync(...params).catch((error) => { if (!this.options.onError) { console.error(error); } }); }
|
run本质就是执行 runAsync ,只不过不需要我们再手动处理异常了。
cancel
1 2 3 4 5 6 7 8
| cancel() { this.count += 1; this.setState({ loading: false, }); this.runPluginHandler('onCancel'); }
|
cancel 就是在 runAsync 说到的手动取消的方法,他改变了 count 的值,导致 count 和 currentCount 不相等,跳出 runAsync。
refresh
1 2 3 4
| refresh() { this.run(...(this.state.params || [])); }
|
refresh 刷新,使用上次请求的参数重新请求一次。
refreshAsync
1 2 3 4
| refreshAsync() { return this.runAsync(...(this.state.params || [])); }
|
refresh 对应 run ,refreshAsync 对应 runAsync。
mutate
1 2 3 4 5 6 7 8
| mutate(data ? : TData | ((oldData?: TData) => TData | undefined)) { const targetData = isFunction(data) ? data(this.state.data) : data; this.runPluginHandler('onMutate', targetData); this.setState({ data: targetData, }); }
|
修改 data 的值,传入的可以是个值,也可以是个函数。
总结
我们来梳理一下 useRequest 到底做了什么,他本身返回的是 useRequestImplement 执行结果,传入了真正的请求方法,用户自定义配置,以及一个插件数组,而 useRequestImplement 又依赖于 Fetch 这个构造方法。整个请求过程都有一条时间线,插件执行的结果就是每个时间节点对应要做的操作,通过 runPluginHandler 统一触发执行,在有的节点允许用户自定义要执行的方法,关于请求的数据的被存在 Fetch 的 state 中,是不是有点像之前的 Redux,最后再将用户操作和请求有关的数据暴露出去。