ahooks source code series (2): useRequest, 10,000-character long text, great benefits after reading it

useRequest schema

First of all, before reading the source code, we must first understand the structure of useRequest, please see the following figure:

image.png

It is divided into two core and two auxiliary modules

The two cores are: Fetchclass and pluginplug-in mechanism

The two auxiliary modules are: type.tstype definition, utilsutility method

The focus is on Fetchthe class and pluginthe plug-in mechanism, which reduces the coupling between each function and its own complexity through the plug-in mechanism, and handles the entire request lifecycle process through the Fetch class . The relationship between the two is that Fetch is the core and main process, and plugin assists Fetch to expand additional functions

Next, let's analyze the source code of useRequest step by step

entry function

useRequset

First, useRequset acts as the main entry function and accepts three parameters

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;

in:

  • service: The function of the request interface
  • options: Configuration items, such as manual, onBefore, etc., see the official document ahooks for details
  • plugins: list of plugins. Note: This parameter has not been exposed in the official document, maybe it is not intended to be used by developers yet, but we can find that the plugins parameter will be extended to the useRequestImplement function , and it is estimated that developers can follow specific rules in the future. define plugin

let's seeuseRequestImplement

useRequestImplement

The source code of useRequestImplement is not too much, it can be directly divided into three parts

function useRequestImplement<TData, TParams extends any[]>(
  service: Service<TData, TParams>,
  options: Options<TData, TParams> = {},
  plugins: Plugin<TData, TParams>[] = [],
) {
  //第一部分:处理参数
  //1.1
  const { manual = false, ...rest } = options;
  const fetchOptions = {
    manual,
    ...rest,
  };
  //1.2
  const serviceRef = useLatest(service);
  const update = useUpdate();
  
  //第二部分:创建 Fetch 实例对象
  const fetchInstance = useCreation(() => {
    //2.1
    const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
    return new Fetch<TData, TParams>(
      serviceRef,
      fetchOptions,
      update,
      Object.assign({}, ...initState),
    );
  }, []);
  fetchInstance.options = fetchOptions;
  //2.2
  // run all plugins hooks
  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(() => {
    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>;
}

export default useRequestImplement;

Let's sort out what each part does:

  • 第一部分:就是处理了一下入参 options,默认 manual 为 false(也就是自动发起请求),然后用 useLatest 保证每次的 service 都是最新的,然后记录了一个 useUpdate 函数,这个函数的代码如下:
import { useCallback, useState } from 'react';

const useUpdate = () => {
  const [, setState] = useState({});

  return useCallback(() => setState({}), []);
};

export default useUpdate;

其实很简单,它类似 class 组件的 forceUpdate,就是通过 setState,让组件强行渲染

  • 第二部分:创建 Fetch 实例对象,然后需要注意两部分

    • 步骤 2.1:它遍历了传入的 plugins 插件数组,如果当前插件有 onInit 方法,就执行,然后存储执行的结果赋值给 initState。而在所有内置的 plugin 中,目前只有 useAutoRunPluginonInit 方法,这个方法返回的就是一个包含 loading 属性的对象

    image.png

    • 步骤2.2:这一步,也遍历 plugins 数组,然后传入 fetchInstance 实例对象和 fecthOptions 配置项,将每个插件的执行结果存入到数组中,然后把这个结果数组挂载到 fetchInstance.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;
    }
    

    返回的是个对象,包含 onBefore、onRequest、onSuccess、onError、onFinally、onCancel、onMutate 这几个钩子,从名字上看,对应的是请求流程的各个生命周期。也就是说 pluginImpls 里面存的是一堆含有各个钩子函数的对象的数组,而这其实是发布订阅模式,他们会在请求的不同生命周期中被调用,我们稍后在后面 Fetch 流程里面再细说

    • 第三部分:处理一下请求参数,因为可能有缓存之类的考虑,然后就看是自动请求还是手动请求,最后返回数据对象

到这里,useRequestImplement 也梳理完了。接下来,我们看重点 Fetch 类的实现

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(
    public serviceRef: MutableRefObject<Service<TData, TParams>>, // 用户传入的请求接口的函数
    public options: Options<TData, TParams>, // 这个就是用户传入的 options
    public subscribe: Subscribe,  //这个 subscribe 就是之前那个 useUpdate,强制刷新组件的
    public initState: Partial<FetchState<TData, TParams>> = {},  // 这个就是 { loading: !manual && ready }
  ) {
    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)) {
    // ...
  }
}

Fetch 类也挺简单的,提供了一些 API 用于处理请求,只不过像 run、runAsync、cancel、refresh 等是暴露给开发者的,而 setState、runPluginHandler是内部的 API。然后维护的数据通过 setState 方法设置数据,设置完成通过 subscribe(也就是 useUpdate) 调用通知 useRequestImplement 组件重新渲染,从而获取最新值。(所以这里状态更新的时候,都会导致组件重新渲染)。而我们重点关注 runAsync、runPluginHandler,因为其他的 API 基本都会调用这两个方法

我们先来看 runPluginHandler

runPluginHandler

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);
}

它会去遍历 pluginImpls,先前我们说了,pluginImpls 是个数组,里面每个元素是个包含了一个或多个钩子的对象,也就是 { onBefore?, onRequest?, onSuccess?, onError?, onFinally?, onCancel?、onMutate? },然后 event 其实就是这些钩子的 key,即 keyof PluginReturn<TData, TParams>,也就是说,如果当前这个插件支持这个 event,那就去执行对应的钩子,其中某些钩子可能有返回值,就记录下来,最后通过 Object.assign 合并结果。

通过插件化的方式,Fetch 类只需要完成整体流程的功能,所有额外的功能(比如重试、轮询等等)都交给插件去实现。这么做符合职责单一原则:一个 Plugin 只做一件事,相互之间不相关。整体的可维护性更高,并且拥有更好的可测试性。

通过上面的分析可以看出:基本所有的插件功能都是在一个请求的一个或者多个生命周期中实现的,也就是说我们只需要在请求的相应阶段,执行插件的逻辑,就能执行和完成插件的功能

图解如下:

image.png

runAsync

runAsync 就是整个 Fetch 流程的主心骨了,我们来看看它的代码实现

async runAsync(...params: TParams): Promise<TData> {
    //1.1
    this.count += 1;
    const currentCount = this.count;
    
    //1.2
    const {
      stopNow = false,
      returnNow = false,
      ...state
    } = this.runPluginHandler('onBefore', params);

    //1.3 stop request
    if (stopNow) {
      return new Promise(() => {});
    }
  
    //1.4
    this.setState({
      loading: true,
      params,
      ...state,
    });

    //1.5 return now
    if (returnNow) {
      return Promise.resolve(state.data);
    }

    //1.6
    this.options.onBefore?.(params);

    try {
      //1.7 replace service
      let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);
      if (!servicePromise) {
        servicePromise = this.serviceRef.current(...params);
      }
      const res = await servicePromise;

      //1.8
      if (currentCount !== this.count) {
        // prevent run.then when request is canceled
        return new Promise(() => {});
      }
      this.setState({
        data: res,
        error: undefined,
        loading: false,
      });

      //1.9
      this.options.onSuccess?.(res, params);
      this.runPluginHandler('onSuccess', res, params);

      //1.10
      this.options.onFinally?.(params, res, undefined);
      if (currentCount === this.count) {
        this.runPluginHandler('onFinally', params, res, undefined);
      }

      return res;
    } catch (error) {
      //1.11
      if (currentCount !== this.count) {
        // prevent run.then when request is canceled
        return new Promise(() => {});
      }
      this.setState({
        error,
        loading: false,
      });

      //1.12
      this.options.onError?.(error, params);
      this.runPluginHandler('onError', error, params);

      //1.13
      this.options.onFinally?.(params, undefined, error);
      if (currentCount === this.count) {
        this.runPluginHandler('onFinally', params, undefined, error);
      }

      throw error;
    }
  }

这一部分代码看起来可能有点多,但一句话概括就是:调用我们请求接口数据的方法,然后拿到成功或者失败的结果,对数据进行处理,然后更新 state,然后执行插件的各回调钩子,还有我们通过 options 传入的回调函数

接下来我们来稍微分析一下:

步骤1.1:

我们可以看到 1.1 有 this.countcurrentCount 两个变量,这两个变量其实和 cancel() 取消请求有关系,你看 1.8、1.10、1.11 都会判断 this.count 和 currentCount 是否相等决定是否取消请求

步骤1.2:

这一步可以发现是去调 runPluginHandler,然后执行的是 onBefore 钩子。我们先看看哪些插件返回的对象里包含 onBefore 钩子:

image.png

有五个插件会返回包含 onBefore 钩子的对象,但在 useRequestImplement 中的 fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions)); 这一步时,插件一定会返回包含 onBefore 钩子的对象吗?

我们来看看这几个插件的内部实现:

useAutoRunPlugin: image.png

useCachePlugin:

image.png

useLoadingDelayPlugin: image.png

剩下两个插件一样的,只有在满足条件的时候才会返回包含 onBefore 的对象。现在我们回到步骤 1.2 来:

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);
}

//步骤 1.2
const {
      stopNow = false,
      returnNow = false,
      ...state
    } = this.runPluginHandler('onBefore', params);

假如现在我只传了 loadingDelay 参数,只做一个延时效果,这个效果由useLoadingDelayPlugin 插件完成,那么我在 useRequestImplement 入口函数时,fetchInstance.pluginImpls 数组中也就只有一个对象会包含 onBefore 钩子(该对象由 useLoadingDelayPlugin 返回),然后在步骤 1.2 遍历 pluginImpls 时,也就只会执行延时效果的 onBefore 钩子

image.png

步骤1.3

就是看要不要停止请求

步骤1.4

不停止请求就设置参数、组件状态

步骤1.5

这个应该跟缓存有关,要不要立即返回数据

步骤1.6

执行我们传入的 options.onBefore

剩余步骤

剩下的步骤就是执行插件的钩子,然后执行 options 传入的回调,最终返回结果

错误抓捕

这里提一嘴,runAsync 用了 try catch 捕获异常,返回的是个 Promise,当出错时,需要自己捕获异常

run 方法之所以不需要我们自己捕获异常是因为 run 方法其实调用的就是 runAsync 方法,然后帮我们做了异常处理

 run(...params: TParams) {
    this.runAsync(...params).catch((error) => {
      if (!this.options.onError) {
        console.error(error);
      }
    });
  }

总结

分析之后,Fetch 做的事很简单,就是在请求的生命周期中,调用插件相关的钩子,然后更新数据到 state,如果有 options 的回调函数,调用即可

Plugins

在前面 步骤 1.2 里面我们分析了插件是如何能在请求的过程中被执行的,这里我们就来看看插件源码的逻辑。因为插件比较多,我们就拿一个简单的插件 useLoadingDelayPlugin 来看看

图解如下:

image.png

总之,一个插件只做一件事,负责单一功能。

最后

The core process of useRequest is in the Fetch class. In this class, through the form of life cycle process, the corresponding hooks of different plug-ins are executed to realize different functions. Coupling the relationship between functions, reducing the complexity of its own maintenance, and improving testability

In fact, the plug-in mechanism of useRequest is worth learning. For example, when we encapsulate the general hook, we need to do it in the 对外简单direction 隐藏内部复杂of . 深模块to learn

Guess you like

Origin juejin.im/post/7246963275955454009