本文是深入浅出 ahooks 源码系列文章的第二篇,这个系列的指标次要有以下几点:
- 加深对 React hooks 的了解。
- 学习如何形象自定义 hooks。构建属于本人的 React hooks 工具库。
- 造就浏览学习源码的习惯,工具库是一个对源码浏览不错的抉择。
注:本系列对 ahooks 的源码解析是基于 v3.3.13
。本人 folk 了一份源码,次要是对源码做了一些解读,可见 详情。
系列文章:
- 大家都能看得懂的源码(一)ahooks 整体架构篇
本文来讲下 ahooks 的外围 hook —— useRequest。
useRequest 简介
依据官网文档的介绍,useRequest 是一个弱小的异步数据管理的 Hooks,React 我的项目中的网络申请场景应用 useRequest 就够了。
useRequest 通过插件式组织代码,外围代码极其简略,并且能够很不便的扩大出更高级的性能。目前已有能力包含:
- 主动申请 / 手动申请
- 轮询
- 防抖
- 节流
- 屏幕聚焦从新申请
- 谬误重试
- loading delay
- SWR(stale-while-revalidate)
- 缓存
这里能够看到 useRequest 的性能是十分弱小的,如果让你来实现,你会如何实现?也能够从介绍中看到官网的答案——插件化机制。
架构
如上图所示,我把整个 useRequest 分成了几个模块。
- 入口 useRequest。它负责的是初始化解决数据以及将后果返回。
- Fetch。是整个 useRequest 的外围代码,它解决了整个申请的生命周期。
- plugin。在 Fetch 中,会通过插件化机制在不同的机会触发不同的插件办法,拓展 useRequest 的性能个性。
- utils 和 types.ts。提供工具办法以及类型定义。
useRequest 入口解决
先从入口文件开始,packages/hooks/src/useRequest/src/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, [
// 插件列表,用来拓展性能,个别用户不应用。文档中没有看到裸露 API
...(plugins || []),
useDebouncePlugin,
useLoadingDelayPlugin,
usePollingPlugin,
useRefreshOnWindowFocusPlugin,
useThrottlePlugin,
useAutoRunPlugin,
useCachePlugin,
useRetryPlugin,
] as Plugin<TData, TParams>[]);
}
export default useRequest;
这里第一(service 申请实例)第二个参数(配置选项),咱们比拟相熟,第三个参数文档中没有提及,其实就是插件列表,用户能够自定义插件拓展性能。
能够看到返回了 useRequestImplement
办法。次要是对 Fetch 类进行实例化。
const update = useUpdate();
// 保障申请实例都不会产生扭转
const fetchInstance = useCreation(() => {
// 目前只有 useAutoRunPlugin 这个 plugin 有这个办法
// 初始化状态,返回 {loading: xxx},代表是否 loading
const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
// 返回申请实例
return new Fetch<TData, TParams>(
serviceRef,
fetchOptions,
// 能够 useRequestImplement 组件
update,
Object.assign({}, ...initState),
);
}, []);
fetchInstance.options = fetchOptions;
// run all plugins hooks
// 执行所有的 plugin,拓展能力,每个 plugin 中都返回的办法,能够在特定机会执行
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
实例化的时候,传参顺次为申请实例,options 选项,父组件的更新函数,初始状态值。
这里须要十分注意的一点是最初一行,它执行了所有的 plugins 插件,传入的是 fetchInstance 实例以及 options 选项,返回的后果赋值给 fetchInstance 实例的 pluginImpls
。
另外这个文件做的就是将后果返回给开发者了,这点不细说。
Fetch 和 Plugins
接下来最外围的源码局部 —— Fetch 类。其代码不多,算是十分精简,先简化一下:
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(
// React.MutableRefObject —— useRef 创立的类型,能够批改
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, // 非手动,就 loading
...initState,
};
}
// 更新状态
setState(s: Partial<FetchState<TData, TParams>> = {}) {
this.state = {
...this.state,
...s,
};
this.subscribe();}
// 执行插件中的某个事件(event),rest 为参数传入
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {// 省略代码...}
// 如果设置了 options.manual = true,则 useRequest 不会默认执行,须要通过 run 或者 runAsync 来触发执行。// runAsync 是一个返回 Promise 的异步函数,如果应用 runAsync 来调用,则意味着你须要本人捕捉异样。async runAsync(...params: TParams): Promise<TData> {// 省略代码...}
// run 是一个一般的同步函数,其外部也是调用了 runAsync 办法
run(...params: TParams) {// 省略代码...}
// 勾销以后正在进行的申请
cancel() {// 省略代码...}
// 应用上一次的 params,从新调用 run
refresh() {// 省略代码...}
// 应用上一次的 params,从新调用 runAsync
refreshAsync() {// 省略代码...}
// 批改 data。参数能够为函数,也能够是一个值
mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {// 省略代码...}
state 以及 setState
在 constructor 中,次要是进行了数据的初始化。其中保护的数据次要蕴含一下几个重要的数据以及通过 setState 办法设置数据,设置实现通过 subscribe 调用告诉 useRequestImplement 组件从新渲染,从而获取最新值。
// 几个重要的返回值
state: FetchState<TData, TParams> = {
loading: false,
params: undefined,
data: undefined,
error: undefined,
};
// 更新状态
setState(s: Partial<FetchState<TData, TParams>> = {}) {
this.state = {
...this.state,
...s,
};
this.subscribe();}
插件化机制的实现
上文有提到所有的插件运行的后果都赋值给 pluginImpls。它的类型定义如下:
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;
}
除了最初一个 onMutate 之外,能够看到返回的办法都是在一个申请的生命周期中的。一个申请从开始到完结,如下图所示:
如果你比拟认真,你会发现 根本所有的插件性能都是在一个申请的一个或者多个阶段中实现的,也就是说咱们只须要在申请的相应阶段,执行咱们的插件的逻辑,就能实现咱们插件的性能。
执行特定阶段插件办法的函数为 runPluginHandler,其 event 入参就是下面 PluginReturn key 值。
// 执行插件中的某个事件(event),rest 为参数传入
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);
}
通过这样的形式,Fetch 类的代码会变得十分的精简,只须要实现整体流程的性能,所有额定的性能(比方重试、轮询等等)都交给插件去实现。这么做的长处:
- 合乎职责繁多准则。一个 Plugin 只做一件事,相互之间不相干。整体的可维护性更高,并且领有更好的可测试性。
- 合乎深模块的软件设计理念。其认为最好的模块提供了弱小的性能,又有着简略的接口。试想每个模块由一个长方形示意,如下图,长方形的面积大小和模块实现的性能多少成比例。顶部边代表模块的接口,边的长度代表它的复杂度。最好的模块是深的:他们有很多性能暗藏在简略的接口后。深模块是好的形象,因为它只把本人外部的一小部分复杂度裸露给了用户。
外围办法 —— runAsync
能够看到 runAsync 是运行申请的最外围办法,其余的办法比方 run/refresh/refreshAsync
最终都是调用该办法。
并且该办法中就能够看到整体申请的生命周期的解决。这跟下面插件返回的办法设计是保持一致的。
申请前 —— onBefore
解决申请前的状态,并执行 Plugins 返回的 onBefore 办法,并依据返回值执行相应的逻辑。比方,useCachePlugin 如果还存于陈腐工夫内,则不必申请,返回 returnNow,这样就会间接返回缓存的数据。
this.count += 1;
// 次要为了 cancel 申请
const currentCount = this.count;
const {
stopNow = false,
returnNow = false,
...state
// 先执行每个插件的前置函数
} = this.runPluginHandler('onBefore', params);
// stop request
if (stopNow) {return new Promise(() => {});
}
this.setState({
// 开始 loading
loading: true,
// 申请参数
params,
...state,
});
// return now
// 立刻返回,跟缓存策略无关
if (returnNow) {return Promise.resolve(state.data);
}
// onBefore - 申请之前触发
// 如果有缓存数据,则间接返回
this.options.onBefore?.(params);
进行申请——onRequest
这个阶段只有 useCachePlugin 执行了 onRequest 办法,执行后返回 service Promise(有可能是缓存的后果),从而达到缓存 Promise 的成果。
// replace service
// 如果有 cache 的实例,则应用缓存的实例
let {servicePromise} = this.runPluginHandler('onRequest', this.serviceRef.current, params);
if (!servicePromise) {servicePromise = this.serviceRef.current(...params);
}
const res = await servicePromise;
useCachePlugin 返回的 onRequest 办法:
// 申请阶段
onRequest: (service, args) => {
// 看 promise 有没有缓存
let servicePromise = cachePromise.getCachePromise(cacheKey);
// If has servicePromise, and is not trigger by self, then use it
// 如果有 servicePromise,并且不是本人触发的,那么就应用它
if (servicePromise && servicePromise !== currentPromiseRef.current) {return { servicePromise};
}
servicePromise = service(...args);
currentPromiseRef.current = servicePromise;
// 设置 promise 缓存
cachePromise.setCachePromise(cacheKey, servicePromise);
return {servicePromise};
},
勾销申请 —— onCancel
刚刚在申请开始前定义了 currentCount 变量,其实为了 cancel 申请。
this.count += 1;
// 次要为了 cancel 申请
const currentCount = this.count;
在申请过程中,开发者能够调用 Fetch 的 cancel 办法:
// 勾销以后正在进行的申请
cancel() {
// 设置 + 1,在执行 runAsync 的时候,就会发现 currentCount !== this.count,从而达到勾销申请的目标
this.count += 1;
this.setState({loading: false,});
// 执行 plugin 中所有的 onCancel 办法
this.runPluginHandler('onCancel');
}
这个时候,currentCount !== this.count,就会返回空数据。
// 如果不是同一个申请,则返回空的 promise
if (currentCount !== this.count) {
// prevent run.then when request is canceled
return new Promise(() => {});
}
最初后果解决——onSuccess/onError/onFinally
这部分也就比较简单了,通过 try…catch… 最初胜利,就间接在 try 开端加上 onSuccess 的逻辑,失败在 catch 开端加上 onError 的逻辑,两者都加上 onFinally 的逻辑。
try {
const res = await servicePromise;
// 省略代码...
this.options.onSuccess?.(res, params);
// plugin 中 onSuccess 事件
this.runPluginHandler('onSuccess', res, params);
// service 执行实现时触发
this.options.onFinally?.(params, res, undefined);
if (currentCount === this.count) {
// plugin 中 onFinally 事件
this.runPluginHandler('onFinally', params, res, undefined);
}
return res;
// 捕捉报错
} catch (error) {
// 省略代码...
// service reject 时触发
this.options.onError?.(error, params);
// 执行 plugin 中的 onError 事件
this.runPluginHandler('onError', error, params);
// service 执行实现时触发
this.options.onFinally?.(params, undefined, error);
if (currentCount === this.count) {
// plugin 中 onFinally 事件
this.runPluginHandler('onFinally', params, undefined, error);
}
// 抛出谬误。// 让内部捕捉感知谬误
throw error;
}
思考与总结
useRequest 是 ahooks 最外围的性能之一,它的性能十分丰盛,但外围代码(Fetch 类)绝对简略,这得益于它的插件化机制,把特定性能交给特定的插件去实现,本人只负责主流程的设计,并裸露相应的执行机会即可。
这对于咱们平时的组件 /hook 封装很有帮忙,咱们对一个简单性能的形象,能够尽可能保障对外接口简略。外部实现须要遵循繁多职责的准则,通过相似插件化的机制,细化拆分组件,从而晋升组件可维护性、可测试性。
参考
- 软件设计之 Deep Module(深模块)
- 精读 ahooks useRequest 源码
本文由 mdnice 多平台公布