5

This article is the second in a series of in-depth ahoos source code articles, which have been organized into document- address . I think it's not bad, give me a follow to support it, thanks.

This article will talk about the core hook of ahooks - useRequest.

Introduction to useRequest

According to the introduction of the official documentation, useRequest is a powerful Hooks for asynchronous data management. It is enough to use useRequest for network request scenarios in React projects.

useRequest organizes code through plug-ins , the core code is extremely simple, and it can be easily extended to more advanced functions. Current capabilities include:

  • Automatic request/manual request
  • polling
  • anti-shake
  • throttling
  • Screen focus re-request
  • error retry
  • loading delay
  • SWR(stale-while-revalidate)
  • cache

Here you can see that the function of useRequest is very powerful. If you were asked to implement it, how would you implement it? You can also see the official answer from the introduction - the plug-in mechanism.

Architecture


As shown above, I divided the entire useRequest into several modules.

  • Entry useRequest. It is responsible for initializing the processing data and returning the result.
  • Fetch. It is the core code of the entire useRequest, which handles the entire life cycle of the request.
  • plugin. In Fetch, different plug-in methods are triggered at different times through the plug-in mechanism to expand the functional characteristics of useRequest.
  • utils and types.ts. Provides utility methods and type definitions.

useRequest entry processing

Start with the entry file, 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;

Here, the first (service request instance) and the second parameter (configuration option) are familiar to us. The third parameter is not mentioned in the document. It is actually a list of plugins. Users can customize the extension function of the plugin.

You can see that the useRequestImplement method is returned. Mainly instantiate the Fetch class.

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

When instantiated, the parameters passed are the request instance, options option, update function of the parent component, and initial state value.

One thing to pay attention to here is the last line, which executes all plugins plugins, passes in the fetchInstance instance and options options, and assigns the returned result to the fetchInstance instance pluginImpls .

In addition, what this file does is to return the result to the developer, which is not discussed in detail.

Fetch and Plugins

The next core part of the source code - the Fetch class. Its code is not much, it is very concise, let's simplify it first:

 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 and setState

In the constructor, the data initialization is mainly carried out. The data maintained mainly includes the following important data and the data set by the setState method. After the setting is completed, the useRequestImplement component is notified by the subscribe call to re-render, so as to obtain the latest value.

 // 几个重要的返回值
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();
}

Implementation of plug-in mechanism

As mentioned above, the results of all plugin operations are assigned to pluginImpls. Its type is defined as follows:

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

Except for the last onMutate, you can see that the returned methods are all in the life cycle of a request. A request goes from start to finish, as shown in the following figure:

If you are more careful, you will find that basically all plug-in functions are implemented in one or more stages of a request, that is to say, we only need to execute the logic of our plug-in in the corresponding stage of the request to complete The functionality of our plugin .

The function that executes the plug-in method at a specific stage is runPluginHandler, and its event input parameter is the value of the PluginReturn key above.

 // 执行插件中的某个事件(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);
}

In this way, the code of the Fetch class will become very streamlined, and only the functions of the overall process need to be completed, and all additional functions (such as retry, polling, etc.) are handed over to the plugin to implement. Advantages of doing this:

  • Comply with the principle of single responsibility . A Plugin does only one thing and is not related to each other. The overall maintainability is higher, and it has better testability.
  • In line with the deep module software design concept. It believes that the best modules provide powerful functionality with a simple interface. Imagine that each module is represented by a rectangle, as shown in the figure below. The size of the rectangle is proportional to the function implemented by the module. The top edge represents the interface of the module, and the length of the edge represents its complexity. The best modules are deep: they have a lot of functionality hidden behind simple interfaces. A deep module is a good abstraction because it exposes only a small fraction of its internal complexity to the user.

Core method - runAsync

It can be seen that runAsync is the core method of running the request, and other methods such as run/refresh/refreshAsync ultimately call this method.

And in this method, you can see the processing of the life cycle of the overall request. This is consistent with the method design returned by the plugin above.

Before request - onBefore

Process the state before the request, execute the onBefore method returned by Plugins, and execute the corresponding logic according to the return value. For example, if useCachePlugin still exists in the fresh time, it does not need to request and returns returnNow, which will directly return the cached data.

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

Make a request - onRequest

At this stage, only useCachePlugin executes the onRequest method, and returns the service Promise (possibly the result of the cache) after execution, so as to achieve the effect of caching the Promise.

 // replace service
// 如果有 cache 的实例,则使用缓存的实例
let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);

if (!servicePromise) {
  servicePromise = this.serviceRef.current(...params);
}

const res = await servicePromise;

The onRequest method returned by useCachePlugin:

 // 请求阶段
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 };
},

Cancel the request - onCancel

The currentCount variable was just defined before the request started, in fact, for the cancel request.

 this.count += 1;
// 主要为了 cancel 请求
const currentCount = this.count;

During the request process, the developer can call the cancel method of Fetch:

 // 取消当前正在进行的请求
cancel() {
  // 设置 + 1,在执行 runAsync 的时候,就会发现 currentCount !== this.count,从而达到取消请求的目的
  this.count += 1;
  this.setState({
    loading: false,
  });

  // 执行 plugin 中所有的 onCancel 方法
  this.runPluginHandler('onCancel');
}

At this time, currentCount !== this.count, will return empty data.

 // 假如不是同一个请求,则返回空的 promise
if (currentCount !== this.count) {
  // prevent run.then when request is canceled
  return new Promise(() => {});
}

Final result processing - onSuccess/onError/onFinally

This part is relatively simple, through try...catch... In the end, the logic of onSuccess is added directly at the end of try, the logic of onError is added at the end of catch when it fails, and the logic of onFinally is added to both.

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

Thinking and Summarizing

useRequest is one of the core functions of ahooks. Its functions are very rich, but the core code (Fetch class) is relatively simple. Thanks to its plug-in mechanism, specific functions are handed over to specific plug-ins for implementation, and they are only responsible for The design of the main process and the exposure of the corresponding execution timing can be done.

This is very helpful for our usual component/hook encapsulation. We can abstract a complex function and keep the external interface as simple as possible. The internal implementation needs to follow the principle of single responsibility, and refine and split components through a plug-in-like mechanism, thereby improving the maintainability and testability of components.


Gopal
366 声望77 粉丝