ahooks 想必大家都用过,目前还有了 vue 的移植版,他是一个高质量的 hooks 库,其中 useRequest 应该大家都用过或听说过,这里就从这个为切入点,源码解析一下它的实现原理。
基本使用
声明:
const getImageInfoService = (params) => {
return GetImageInfo({ ImageIds: params.imageIds }); // promise
};
const { run, cancel } = useRequest(getImageInfoService, {
manual: true,
pollingInterval: 3000,
pollingErrorRetryCount: 3,
onSuccess: (resp) => {
// 请求返回的值
},
onError: (e) => {
console.log(e);
}
});
调用:
run({ imageIds });
上面举了一个获取图片信息的例子,可以看到他可以用于轮询(polling),使用很方便。
源码解析
来看一下源码的目录结构:
从入口开始看
// 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>[]);
}
外层引入了自定义的插件,useRequestImplement 是内部的实现。
useRequestImplement 中,首先接收 option 的参数:
const { manual = false, ready = true, ...rest } = options;
然后是最核心的代码,请求实例的声明:
// useLatest 就是 useref 的一个封装,将传入的 service 放在 useRef 的 current 里再返回,这样写有什么用?这样写可以避免闭包问题和props重渲染
const serviceRef = useLatest(service);
// useUpdate 只是返回一个 useCallback 包裹的空函数
const update = useUpdate();
const fetchOptions = {
manual,
ready,
...rest,
};
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));
其中工具函数 useCreation 定义如下:
export default function useCreation<T>(factory: () => T, deps: DependencyList) {
const { current } = useRef({
deps,
obj: undefined as undefined | T,
initialized: false,
});
if (current.initialized === false || !depsAreSame(current.deps, deps)) {
current.deps = deps;
current.obj = factory();
current.initialized = true;
}
return current.obj as T;
}
depsAreSame 函数是利用 Object.is 来判断依赖是否相同,与react的原理相似。
可以看到,useCreation 在初始化时,或者在依赖更新时,调用 factory 函数;回到 useCreation 这里,就是说初始化时调用一次内置函数,而这个函数,处理了所有的 插件(plugins),初始化插件(onInit) 后,使用了 new Fetch 来创建一个 Fetch 对象,然后将 options 和 pluginImpls 参数挂载在这个对象上。
这个new Fetch我们暂时先不管,我们接着往下看看 useRequestImplement 还有哪些实现:
在加载和卸载时触发:
useMount(() => {
if (!manual && ready) {
// 将请求参数放在Fetch实例的state
const params = fetchInstance.state.params || options.defaultParams || [];
// @ts-ignore
fetchInstance.run(...params);
}
});
useUnmount(() => {
fetchInstance.cancel();
});
可以看到,在初始化时,如果不是手动调用,则自动将 fetchInstance 设置状态后 run !具体怎么 run 的,一会看一下 Fetch 的实现。
先继续 useRequestImplement 函数,其返回:
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吐给用户的参数。
接下来就只剩 Fetch 对象的实现了,源码中使用 react class 声明方式,看来是刻意将其设置为一个对象,其构造器中初始化了state和loading的值。
看一下他的全部声明:
Run 的实现
先看最重要的 run,他负责跑通这个 promise:
run(...params: TParams) {
this.runAsync(...params).catch((error) => {
if (!this.options.onError) {
console.error(error);
}
});
}
runAsync 又调用了 runPluginHandler:
this.count += 1;
const currentCount = this.count;
const {
stopNow = false,
returnNow = false,
...state
} = this.runPluginHandler('onBefore', params);
...
// options 就是 new Fetch 传入的 fetchOptions
this.options.onBefore?.(params);
看一下 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);
}
他这里是遍历插件来调用,不同的插件有相同的事件名称,都会被调用,有点AOP的感觉... 这就要求给 useRequest 写插件时,需要有相同的生命周期函数。
可以看到,插件执行的 runPluginHandler 函数,执行了传递进来的 event,所以具体的请求内容(axios、fetch等)还得业务代码自己来实现,他只是负责执行你传入的函数并配置了很多的调度功能而已。
这里是在 run 之前,执行插件的 onBefore 钩子。
继续往下看 runAsync:
拿到用户定义的 promise:
// 插件有onRequest,则使用插件返回的 promise,否则使用一开始传进来的 serviceRef
let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);
if (!servicePromise) {
servicePromise = this.serviceRef.current(...params);
}
// 执行
const res = await servicePromise;
hook 里一直都有计数器 count,这里有判断了一下:
if (currentCount !== this.count) {
// prevent run.then when request is canceled
return new Promise(() => {});
}
这个仅作参考,保证计数一致是为了保证操作的原子性。每一个操作,run/cancel等都+1
然后将 promise 返回的值设置到state (这个data 也会返回给用户):
// error 在 catch 里,这里就不解读了,流程一致
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);
}
此时 回调里的 option 的 onSuccess 就能接收到响应值了。
Polling & Cancel 的实现
有请求就有取消,我们看一下他是怎么取消 ajax 请求的:
cancel() {
this.count += 1;
this.setState({
loading: false,
});
// 所有的插件都会执行 onCancel,需要注意
this.runPluginHandler('onCancel');
}
可以看到,取消是使用的插件的能力,传入了 onCancel 来接收消息。我们回头查一下,发现有个插件叫 usePollingPlugin,看来取消和轮询是一体的,我们就一起看了。
上面可以知道,runPluginHandler 会调用插件中传入的方法,我们就重点看一下 usePollingPlugin 的事件声明。
我们总揽一下这个插件:
不出所料,确实有个 onCancel,他调用了 stopPolling 函数。
const stopPolling = () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
unsubscribeRef.current?.();
};
还记得 Fetch 组件声明时调用了 onFinally 事件么,这里看一下:
onFinally: () => {
...
timerRef.current = setTimeout(() => {
// if pollingWhenHidden = false && document is hidden, then stop polling and subscribe revisible
if (!pollingWhenHidden && !isDocumentVisible()) {
unsubscribeRef.current = subscribeReVisible(() => {
fetchInstance.refresh();
});
} else {
fetchInstance.refresh();
}
}, pollingInterval);
}
}
可以看到他启动了一个定时器,不停的执行实例上的 refresh 方法。然后在调用 cancel 方法时,触发了 onCancel 事件,进而关闭这个定时器,从而达到了取消轮询的功能。
Refresh 实现
最后我们再来看一下这个 Refresh 方法:
refresh() {
// @ts-ignore
this.run(...(this.state.params || []));
}
哦吼,没有什么新鲜的,就是 run 重新调用。
总结
最后我们来总结一下 useRequest 的执行流程。
完!源码阅读其实并没有那么难,只要沉下心来,从使用作为切入点来看就能理解了,你学废了嘛!