分析useRequest源码实现

359 阅读4分钟

在日常开发工作中经常用到useRequest,但却不清楚内部的实现原理,这次就从源码层面深入分析 useRequest的实现与设计。

支持的功能

  • 延迟loading
  • 轮询
  • 依赖刷新
  • 屏幕聚焦重新请求
  • 防抖
  • 节流
  • 缓存&SWR
  • 错误重试

useRequest的源码结构

useRequest的核心代码位于ahooks项目的packages/hooks/src/useRequest/目录下,主要包含以下几个文件:

  • useRequest.tsuseRequest的入口文件,内置了一些默认的插件。
  • useRequestImplement.tsuseRequest的实现文件,主要是导出各个方法,具体实现是通过new Fetch创建fetchInstance实例。
  • Fetch.ts:核心内容,Fetch类的定义文件,该类负责实际发起请求和管理请求状态。
  • plugins目录:包含各种插件的实现,用于扩展useRequest的功能。

image.png

useRequest的工作流程

1. 初始化与实例化

当在组件中调用useRequest时,实际上是在调用useRequestImplement函数。useRequestImplement首先会创建并初始化一个Fetch实例(通常命名为fetchInstance),这个实例将负责后续的所有请求操作。

image.png

2. 配置与插件

useRequest支持丰富的配置选项,如manualretryDelay等,这些配置项通过options参数传入useRequestImplement。此外,useRequest还支持插件机制,允许开发者通过传入插件数组来扩展功能。这些插件在useRequestImplement内部被注册到fetchInstance上,并在请求的不同生命周期内(如请求前、请求成功、请求失败等)执行相应的回调。V3版本内置了以下八个插件:

  • useDebouncePlugin:防抖
  • useLoadingDelayPlugin:延迟loading
  • usePollingPlugin:轮询
  • useRefreshOnWindowFocusPlugin:屏幕聚焦重新请求
  • useThrottlePlugin:节流
  • useAutoRunPlugin:依赖刷新
  • useCachePlugin:缓存
  • useRetryPlugin:错误重试

3. 请求执行

请求的执行通过调用fetchInstancerunrunAsync方法来完成。这些方法内部会按照预设的流程(如onBeforeonRequestonSuccessonErroronFinally)执行相应的插件回调,并最终发起HTTP请求。请求的状态(如loadingdataerror)会被实时更新并暴露给组件。

import { isFunction } from '../../utils';
import type { MutableRefObject } from 'react';
import type { FetchState, Options, PluginReturn, Service, Subscribe } from './types';

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>,
    public subscribe: Subscribe,
    public initState: Partial<FetchState<TData, TParams>> = {},
  ) {
    this.state = {
      ...this.state,
      loading: !options.manual,
      ...initState,
    };
  }

  setState(s: Partial<FetchState<TData, TParams>> = {}) {
    this.state = {
      ...this.state,
      ...s,
    };
    this.subscribe();
  }

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

  async runAsync(...params: TParams): Promise<TData> {
    this.count += 1;
    const currentCount = this.count;

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

    // stop request
    if (stopNow) {
      return new Promise(() => {});
    }

    this.setState({
      loading: true,
      params,
      ...state,
    });

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

    this.options.onBefore?.(params);

    try {
      // replace service
      let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);

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

      const res = await servicePromise;

      if (currentCount !== this.count) {
        // prevent run.then when request is canceled
        return new Promise(() => {});
      }

      // const formattedResult = this.options.formatResultRef.current ? this.options.formatResultRef.current(res) : res;

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

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

      this.setState({
        error,
        loading: false,
      });

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

      this.options.onFinally?.(params, undefined, error);

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

      throw error;
    }
  }

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

  cancel() {
    ……
  }

  refresh() {
    ……
  }

  refreshAsync() {
    // @ts-ignore
    return this.runAsync(...(this.state.params || []));
  }

  mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
    const targetData = isFunction(data) ? data(this.state.data) : data;
    this.runPluginHandler('onMutate', targetData);
    this.setState({
      data: targetData,
    });
  }
}

4. 请求取消与刷新

useRequest提供了cancelrefresh方法来分别取消当前请求和重新发起请求。cancel方法通过修改内部计数器来实现请求的取消,而refresh方法则相当于重新调用run方法。

cancel() {
    this.count += 1;
    this.setState({
      loading: false,
    });

    this.runPluginHandler('onCancel');
}

refresh() {
    // @ts-ignore
    this.run(...(this.state.params || []));
}

Fetch类的核心方法

Fetch类是useRequest实现的核心,它封装了请求的逻辑和状态管理。以下是几个关键的方法:

  • constructor:构造函数,负责初始化Fetch实例的状态和配置。
  • runAsync:异步执行请求的方法,按照预设的流程执行插件回调,并处理请求结果。
  • cancel:取消当前请求的方法,通过修改内部计数器来实现。
  • refresh:重新发起请求的方法,内部调用runAsync

插件机制

useRequest的插件机制是其灵活性的重要体现。每个插件都是一个对象,包含一系列生命周期钩子(如onBeforeonRequestonSuccess等),这些钩子在请求的不同阶段被调用。通过插件,开发者可以轻松地扩展useRequest的功能,如添加请求缓存、自动重试、轮询等。虽然文档上没有自定义插件的相关说明,但从源码的上看,useRequest的第三个参数是可以接受插件数组的,demo如下:

import { useRequest } from "ahooks";

const service = (parmas) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('parmas', parmas)
            resolve('响应成功!!')
        }, 1000)
    })
}

const cutomPlugins = (fetchInstance, fetchOptions) => {
    
    // 自定义的实现逻辑
    
    return {
        onMutate (a,b,c) {
            console.log('onMutate', a,b,c)
        },
        onBefore (a,b,c) {
            console.log('onBefore', a,b,c)
        },
        onRequest (a,b,c) {
            console.log('onRequest', a,b,c)
        },
        onSuccess (a,b,c) {
            console.log('onSuccess', a,b,c)
        },
        onFinally (a,b,c) {
            console.log('onFinally', a,b,c)
        },
        onError (a,b,c) {
            console.log('onError', a,b,c)
        },
        onCancel (a,b,c) {
            console.log('onCancel', a,b,c)
        },
    }
}

const UseRequestDemo = () => {
    const res = useRequest(service, {
        manual: false,
        defaultParams: [{
            name: 'candy',
            age: 8
        }]
    }, [cutomPlugins]);
    console.log("res", res);
    return <div>useRequestDemo</div>;
};
export default UseRequestDemo;

总结

useRequest通过其强大的功能和灵活的插件机制,为React开发中的异步请求处理提供了完美的解决方案。从源码层面深入分析useRequest,我们不仅可以看到其背后的巧妙的设计,还能更好地理解其工作原理和内部机制。

源码一比一实现(摘抄)+演示demo