从源码上看useRequest是如何封装的请求hook
useRequest
上一篇文章 ahooks useRequest 手动和自动触发请求避坑总结 只是从使用的角度讲了使用useRequest时 手动触发和自动触发的注意事项,这篇文章再从源码的角度看看内部是如何实现这个功能的。
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接收3个参数,第一个service是真实发请求的函数,该函数返回值是一个Promise,类型定义如下:
export type Service<TData, TParams extends any[]> = (...args: TParams) => Promise<TData>;
options是useRequest支持的所有参数,类型定义如下:
export interface Options<TData, TParams extends any[]> {
manual?: boolean;
onBefore?: (params: TParams) => void;
onSuccess?: (data: TData, params: TParams) => void;
onError?: (e: Error, params: TParams) => void;
// formatResult?: (res: any) => TData;
onFinally?: (params: TParams, data?: TData, e?: Error) => void;
defaultParams?: TParams;
// refreshDeps
refreshDeps?: DependencyList;
refreshDepsAction?: () => void;
// loading delay
loadingDelay?: number;
// polling
pollingInterval?: number;
pollingWhenHidden?: boolean;
pollingErrorRetryCount?: number;
// refresh on window focus
refreshOnWindowFocus?: boolean;
focusTimespan?: number;
// debounce
debounceWait?: number;
debounceLeading?: boolean;
debounceTrailing?: boolean;
debounceMaxWait?: number;
// throttle
throttleWait?: number;
throttleLeading?: boolean;
throttleTrailing?: boolean;
// cache
cacheKey?: string;
cacheTime?: number;
staleTime?: number;
setCache?: (data: CachedData<TData, TParams>) => void;
getCache?: (params: TParams) => CachedData<TData, TParams> | undefined;
// retry
retryCount?: number;
retryInterval?: number;
// ready
ready?: boolean;
// [key: string]: any;
}
第3个参数是内置的插件列表,useRequest中的cache、throttle等功能都是通过插件的方式添加的,但是用户不能添加插件,可以看到options中的配置信息,前面是非插件的,后面是插件的。
useRequestImplement
useRequestImplement是进一步的内部实现,它的源码如下:
function useRequestImplement<TData, TParams extends any[]>(
service: Service<TData, TParams>,
options: Options<TData, TParams> = {},
plugins: Plugin<TData, TParams>[] = [],
) {
// manual 默认是false
const { manual = false, ...rest } = options;
const fetchOptions = {
manual,
...rest,
};
// 保存每次传入最新的service
const serviceRef = useLatest(service);
// 触发重新渲染的方法
const update = useUpdate();
// 创建一个不变的Fetch 实例
const fetchInstance = useCreation(() => {
// 执行plugin的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;
// run all plugins hooks
// 用最新的options 创建所有的插件对象
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(() => {
// 卸载取消Fetch
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>;
}
在这里我们可以看到调用useRequest的返回值,它里边的逻辑有如下几个:
- 1 创建Fetch类的对象fetchInstance,这个Fetch类是内部封装的类,创建的时候调用了所有插件的onInit方法。
- 2 创建之后调用执行所有的插件。
- 3 如果manual不是true,则自动执行一次fetchInstance.run方法 用defaultParams参数。
- 4 卸载的时候调用fetchInstance.cancel方法。
- 5 返回结果,都是fetchInstance上的state和方法。
fetchInstance是核心,所以再来看Fetch类的实现。
Fetch类
首先看构造函数, 接收四个参数:
- serviceRef 是 保存service方法的ref。
- options是用户传递的参数。
- subscribe是每次Fetch中的状态变化时出发的函数,useRequest在实现的时候传的是useUpdate()的返回值,也就是用这个方法可以出发使用useRequest函数的重新渲染。
- initState是初始状态,创建的时候给了插件可以改变初始状态的时机。
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,
};
}
调用构造函数的地方。
const serviceRef = useLatest(service);
const update = useUpdate();
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));
setState 设置state,并触发组件更新,注意这里的state和setState只是Fetch这个类自己的实现,跟React框架没有任何关系。
setState(s: Partial<FetchState<TData, TParams>> = {}) {
this.state = {
...this.state,
...s,
};
this.subscribe();
}
下面来看一下最核心的执行发送请求的过程 是在方法runAsync中:
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(() => {});
}
// 发送前设置loading
this.setState({
loading: true,
params,
...state,
});
// return now
if (returnNow) {
return Promise.resolve(state.data);
}
this.options.onBefore?.(params);
try {
// replace service 执行插件onRequest 可以替换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;
}
}
首先是有一个count,每次发送请求都会+1,然后给这个请求创建一个 currentCount记录当前的count,如果请求结束之后判断currentCount已经小于count值了,那么就证明这个请求已经是过期了,有新的请求是在它之后发送的,所以这个请求就直接忽略了,所以cancel一个请求,就是把count+1,然后把loading状态设置成false,源码如下:
cancel() {
this.count += 1;
this.setState({
loading: false,
});
this.runPluginHandler('onCancel');
}
函数的逻辑可以分成3个部分
- 1 主题逻辑,发送前设置loading,发送成功设置data,发送失败设置error。
- 2 执行用户传递的钩子函数,onSuccess、onFinally、onError。
- 3 执行插件的钩子函数,插件的钩子函数的返回值还能控制请求的过程,onBefore阶段可以直接停止请求发送,可以设置发送前的state,onRequest阶段可以拿到server的ref,并且替换掉发送请求的promise。
run方法就是调用runAsync方法,但是不抛出错误,不返回结果:
run(...params: TParams) {
this.runAsync(...params).catch((error) => {
if (!this.options.onError) {
console.error(error);
}
});
}
refresh方法就是用state中存在的params重新调用run方法
refresh() {
// @ts-ignore
this.run(...(this.state.params || []));
}
refreshAsync方法就是用state中存在的params重新调用runAsync方法
refreshAsync() {
// @ts-ignore
return this.runAsync(...(this.state.params || []));
}
mutate方法是用户可以直接修改state中的data
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,
});
}
首次执行和自动请求参数的处理
首次执行的参数
在这里 首次执行是会取defaultParams中的数据。
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);
}
});
refreshDeps 自动请求的参数
这个参数是在useAutoRunPlugin 中使用的,所以需要看下useAutoRunPlugin是如何实现的依赖项变化自动触发请求.
首先是插件的onInit方法,重写了loading state,默认ready和非手动 则loading是true。
useAutoRunPlugin.onInit = ({ ready = true, manual }) => {
return {
loading: !manual && ready,
};
};
其实让我们自己实现也就是用useEffect 监听所有的依赖项,变动的时候触发请求,注意只是在更新的时候,第一次mount的时候不触发, 所以useAutoRunPlugin的实现也是这样,代码如下:
const useAutoRunPlugin: Plugin<any, any[]> = (
fetchInstance,
{ manual, ready = true, defaultParams = [], refreshDeps = [], refreshDepsAction },
) => {
const hasAutoRun = useRef(false);
hasAutoRun.current = false;
useUpdateEffect(() => {
if (!manual && ready) {
hasAutoRun.current = true;
fetchInstance.run(...defaultParams);
}
}, [ready]);
useUpdateEffect(() => {
if (hasAutoRun.current) {
return;
}
if (!manual) {
hasAutoRun.current = true;
if (refreshDepsAction) {
refreshDepsAction();
} else {
fetchInstance.refresh();
}
}
}, [...refreshDeps]);
return {
onBefore: () => {
if (!ready) {
return {
stopNow: true,
};
}
},
};
};
它使用的useUpdateEffect 就是 只在update的时候触发,第一次mounted的时候不触发。 hasAutoRun变量避免重复触发,但是正常情况应该不会发生,我猜是作者在开发的时候发现一些异常情况所以加上的处理。 还有就是增加了一个ready参数, 如果ready参数是false的话 是不会自动发送请求,但是其实外边的manual如果不是false的话也是会默认发一次, 当ready变成了true之后,会用默认参数进行一次请求,其实用默认参数也不是很符合需求。 所以这种自动触发请求的,最好service就不用参数,service直接用闭包的引用依赖的变量,这样也都是获取的最新值,因为service每次都是最新的函数。
总结一下函数渲染中useRequest中变化和不变化的东西: fetchInstace不变,service每次用传的最新值,所以service使用的闭包变量也是最新的,插件会使用新的options重新生成新的插件对象,所以插件中的options每次也都是最新的。
转载自:https://juejin.cn/post/7249375708920463397