likes
comments
collection
share

从源码上看useRequest是如何封装的请求hook

作者站长头像
站长
· 阅读数 16

useRequest

上一篇文章 ahooks useRequest 手动和自动触发请求避坑总结 只是从使用的角度讲了使用useRequest时 手动触发和自动触发的注意事项,这篇文章再从源码的角度看看内部是如何实现这个功能的。

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;

useRequest接收3个参数,第一个service是真实发请求的函数,该函数返回值是一个Promise,类型定义如下:

export type Service<TData, TParams extends any[]> = (...args: TParams) => Promise<TData>;

options是useRequest支持的所有参数,类型定义如下:

export interface Options<TData, TParams extends any[]> {
  manual?: boolean;

  onBefore?: (params: TParams) => void;
  onSuccess?: (data: TData, params: TParams) => void;
  onError?: (e: Error, params: TParams) => void;
  // formatResult?: (res: any) => TData;
  onFinally?: (params: TParams, data?: TData, e?: Error) => void;

  defaultParams?: TParams;

  // refreshDeps
  refreshDeps?: DependencyList;
  refreshDepsAction?: () => void;

  // loading delay
  loadingDelay?: number;

  // polling
  pollingInterval?: number;
  pollingWhenHidden?: boolean;
  pollingErrorRetryCount?: number;

  // refresh on window focus
  refreshOnWindowFocus?: boolean;
  focusTimespan?: number;

  // debounce
  debounceWait?: number;
  debounceLeading?: boolean;
  debounceTrailing?: boolean;
  debounceMaxWait?: number;

  // throttle
  throttleWait?: number;
  throttleLeading?: boolean;
  throttleTrailing?: boolean;

  // cache
  cacheKey?: string;
  cacheTime?: number;
  staleTime?: number;
  setCache?: (data: CachedData<TData, TParams>) => void;
  getCache?: (params: TParams) => CachedData<TData, TParams> | undefined;

  // retry
  retryCount?: number;
  retryInterval?: number;

  // ready
  ready?: boolean;

  // [key: string]: any;
}

第3个参数是内置的插件列表,useRequest中的cache、throttle等功能都是通过插件的方式添加的,但是用户不能添加插件,可以看到options中的配置信息,前面是非插件的,后面是插件的。

useRequestImplement

useRequestImplement是进一步的内部实现,它的源码如下:

function useRequestImplement<TData, TParams extends any[]>(
  service: Service<TData, TParams>,
  options: Options<TData, TParams> = {},
  plugins: Plugin<TData, TParams>[] = [],
) {
 // manual 默认是false
  const { manual = false, ...rest } = options;

  const fetchOptions = {
    manual,
    ...rest,
  };
    // 保存每次传入最新的service
  const serviceRef = useLatest(service);
  // 触发重新渲染的方法
  const update = useUpdate();
  // 创建一个不变的Fetch 实例
  const fetchInstance = useCreation(() => {
    // 执行plugin的onInit
    const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);

    return new Fetch<TData, TParams>(
      serviceRef,
      fetchOptions,
      update,
      Object.assign({}, ...initState),
    );
  }, []);
  fetchInstance.options = fetchOptions;
  // run all plugins hooks
  // 用最新的options 创建所有的插件对象
  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(() => {
  // 卸载取消Fetch
    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>;
}

在这里我们可以看到调用useRequest的返回值,它里边的逻辑有如下几个:

  • 1 创建Fetch类的对象fetchInstance,这个Fetch类是内部封装的类,创建的时候调用了所有插件的onInit方法。
  • 2 创建之后调用执行所有的插件。
  • 3 如果manual不是true,则自动执行一次fetchInstance.run方法 用defaultParams参数。
  • 4 卸载的时候调用fetchInstance.cancel方法。
  • 5 返回结果,都是fetchInstance上的state和方法。

fetchInstance是核心,所以再来看Fetch类的实现。

Fetch类

首先看构造函数, 接收四个参数:

  • serviceRef 是 保存service方法的ref。
  • options是用户传递的参数。
  • subscribe是每次Fetch中的状态变化时出发的函数,useRequest在实现的时候传的是useUpdate()的返回值,也就是用这个方法可以出发使用useRequest函数的重新渲染。
  • initState是初始状态,创建的时候给了插件可以改变初始状态的时机。
  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,
    };
  }

调用构造函数的地方。

  const serviceRef = useLatest(service);

  const update = useUpdate();

  const fetchInstance = useCreation(() => {
    const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);

    return new Fetch<TData, TParams>(
      serviceRef,
      fetchOptions,
      update,
      Object.assign({}, ...initState),
    );
  }, []);
  fetchInstance.options = fetchOptions;
  // run all plugins hooks
  fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));

setState 设置state,并触发组件更新,注意这里的state和setState只是Fetch这个类自己的实现,跟React框架没有任何关系。

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

下面来看一下最核心的执行发送请求的过程 是在方法runAsync中:

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

   // stop request
   if (stopNow) {
     return new Promise(() => {});
   }
    // 发送前设置loading
   this.setState({
     loading: true,
     params,
     ...state,
   });

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

   this.options.onBefore?.(params);

   try {
     // replace service 执行插件onRequest 可以替换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;
   }
 }

首先是有一个count,每次发送请求都会+1,然后给这个请求创建一个 currentCount记录当前的count,如果请求结束之后判断currentCount已经小于count值了,那么就证明这个请求已经是过期了,有新的请求是在它之后发送的,所以这个请求就直接忽略了,所以cancel一个请求,就是把count+1,然后把loading状态设置成false,源码如下:

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

    this.runPluginHandler('onCancel');
  }

函数的逻辑可以分成3个部分

  • 1 主题逻辑,发送前设置loading,发送成功设置data,发送失败设置error。
  • 2 执行用户传递的钩子函数,onSuccess、onFinally、onError。
  • 3 执行插件的钩子函数,插件的钩子函数的返回值还能控制请求的过程,onBefore阶段可以直接停止请求发送,可以设置发送前的state,onRequest阶段可以拿到server的ref,并且替换掉发送请求的promise。

run方法就是调用runAsync方法,但是不抛出错误,不返回结果:

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

refresh方法就是用state中存在的params重新调用run方法

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

refreshAsync方法就是用state中存在的params重新调用runAsync方法

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

mutate方法是用户可以直接修改state中的data

  mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
    let targetData: TData | undefined;
    if (isFunction(data)) {
      // @ts-ignore
      targetData = data(this.state.data);
    } else {
      targetData = data;
    }

    this.runPluginHandler('onMutate', targetData);

    this.setState({
      data: targetData,
    });
  }

首次执行和自动请求参数的处理

首次执行的参数

在这里 首次执行是会取defaultParams中的数据。

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

refreshDeps 自动请求的参数

这个参数是在useAutoRunPlugin 中使用的,所以需要看下useAutoRunPlugin是如何实现的依赖项变化自动触发请求.

首先是插件的onInit方法,重写了loading state,默认ready和非手动 则loading是true。

useAutoRunPlugin.onInit = ({ ready = true, manual }) => {
  return {
    loading: !manual && ready,
  };
};

其实让我们自己实现也就是用useEffect 监听所有的依赖项,变动的时候触发请求,注意只是在更新的时候,第一次mount的时候不触发, 所以useAutoRunPlugin的实现也是这样,代码如下:

const useAutoRunPlugin: Plugin<any, any[]> = (
  fetchInstance,
  { manual, ready = true, defaultParams = [], refreshDeps = [], refreshDepsAction },
) => {
  const hasAutoRun = useRef(false);
  hasAutoRun.current = false;

  useUpdateEffect(() => {
    if (!manual && ready) {
      hasAutoRun.current = true;
      fetchInstance.run(...defaultParams);
    }
  }, [ready]);

  useUpdateEffect(() => {
    if (hasAutoRun.current) {
      return;
    }
    if (!manual) {
      hasAutoRun.current = true;
      if (refreshDepsAction) {
        refreshDepsAction();
      } else {
        fetchInstance.refresh();
      }
    }
  }, [...refreshDeps]);

  return {
    onBefore: () => {
      if (!ready) {
        return {
          stopNow: true,
        };
      }
    },
  };
};

它使用的useUpdateEffect 就是 只在update的时候触发,第一次mounted的时候不触发。 hasAutoRun变量避免重复触发,但是正常情况应该不会发生,我猜是作者在开发的时候发现一些异常情况所以加上的处理。 还有就是增加了一个ready参数, 如果ready参数是false的话 是不会自动发送请求,但是其实外边的manual如果不是false的话也是会默认发一次, 当ready变成了true之后,会用默认参数进行一次请求,其实用默认参数也不是很符合需求。 所以这种自动触发请求的,最好service就不用参数,service直接用闭包的引用依赖的变量,这样也都是获取的最新值,因为service每次都是最新的函数。

总结一下函数渲染中useRequest中变化和不变化的东西: fetchInstace不变,service每次用传的最新值,所以service使用的闭包变量也是最新的,插件会使用新的options重新生成新的插件对象,所以插件中的options每次也都是最新的。