本文是深入浅出 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 requestif (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,就会返回空数据。

// 如果不是同一个申请,则返回空的 promiseif (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多平台公布