ahooks 源码解析之 useRequest

源码地址:github.com/alibaba/hoo...

前言

我们一看到 request 的字样自然就联想到 useRequest 一定是调后端接口的请求库,其实并不是。

它是一个异步数据管理的 Hooks,也就是说它是用来给异步方法丰富更多强大的功能的,它本身和远程数据请求库是解耦的,它可以配合 axios、原生fetch、甚至只是一个简单的 Promise 都是 OK 的。

个人觉得通过阅读 useRequest源码来入门学习 js 的插件和生命周期设计非常合适,源码本身也比较简单

源码解析

入口文件:

ts 复制代码
import useRequest from './src/useRequest';
import { clearCache } from './src/utils/cache';

export { clearCache };

export default useRequest;

useRequest 对外提供了两个方法,一个是方法本身,一个是 clearCache,它可以让使用者自行清除已缓存的请求数据

useRequst.ts

ts 复制代码
import useAutoRunPlugin from './plugins/useAutoRunPlugin';
import useCachePlugin from './plugins/useCachePlugin';
import useDebouncePlugin from './plugins/useDebouncePlugin';
import useLoadingDelayPlugin from './plugins/useLoadingDelayPlugin';
import usePollingPlugin from './plugins/usePollingPlugin';
import useRefreshOnWindowFocusPlugin from './plugins/useRefreshOnWindowFocusPlugin';
import useRetryPlugin from './plugins/useRetryPlugin';
import useThrottlePlugin from './plugins/useThrottlePlugin';
import type { Options, Plugin, Service } from './types';
import useRequestImplement from './useRequestImplement';

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 可以分成三部分:

  1. useRequestImplement:用来生成最核心的 Fetch 方法及运行插件和生命周期
  2. plugins:以插件的形式实现各种功能
  3. service:外部传入的异步操作方法

useRequestImplement

ts 复制代码
function useRequestImplement<TData, TParams extends any[]>(
  service: Service<TData, TParams>,
  options: Options<TData, TParams> = {},
  plugins: Plugin<TData, TParams>[] = [],
) {
  const { manual = false, ...rest } = options;

  const fetchOptions = { manual, ...rest };

  // useLatest hooks 可以获取到的 service 的最新值,避免闭包问题
  const serviceRef = useLatest(service);

  // 调用 update 方法可以强制刷新 react 组件
  const update = useUpdate();

  // useCreation 类似 useMemo
  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));

  useMount(() => {
    // 如果 manual 为 false,则直接执行 run 方法,发起异步操作
    if (!manual) {
      // useCachePlugin can set fetchInstance.state.params from cache when init
      const params = fetchInstance.state.params || options.defaultParams || [];
      fetchInstance.run(...params);
    }
  });

  useUnmount(() => {
    fetchInstance.cancel();
  });

  // 调用 useRequest 返回的方法对象
  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>;
}

掌握几个核心点:

  1. fetchInstance 为异步操作的核心对象,它返回了 Fetch 类的实例化对象(这个 Fetch 不是那个浏览器原生 Fetch 方法),并接受四个参数
  • serviceRef:异步操作方法
  • fetchOptions:请求的配置参数
  • update:强制更新 react 组件方法
  • initState:初始状态对象 → 通过执行每个 plugin 的 onInit 方法收集整合得到
  1. 执行每个 plugin 方法,传入 fetchInstancefetchOptions ,将执行结果赋值给 fetchInstance.pluginImpls
  2. 在组件首次渲染(useMount)时,如果没有设置 manual,则直接执行 fetchInstance.run 方法执行异步操作
  3. 在组件卸载(useUnmount)时,执行 fetchInstance.cancel 方法

Fetch

Fetch 是核心类,这里分成几个阶段来解析:

初始化

ts 复制代码
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,
    };
  }
}

这里初始化了三个变量:

  1. pluginImpls:保存插件的回调数据对象
  2. count:计数器,具体用法后面会讲
  3. state:内部状态,初始化有 loading、params、data 和 error

同时接受四个参数,serviceRef/ options / subscribe / initState,具体含义上面 fetchInstance 里有讲到,不再赘述。

执行

无论 manual 是否设置,最终都以执行 run 方法为开始,其内部调用了 runAsync 方法传入请求参数 params

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

下面的 runAsync 的源码部分,其中省略了插件逻辑和生命周期,后面会单独讲

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

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

  try {
    const res = await this.serviceRef.current(...params);

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

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

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

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

    throw error;
  }
}

讲 runAsync 之前,我们先看下里面用到的一些方法都干了啥

ts 复制代码
// 设置 state,同时调用 subscribe 方法,触发视图组件更新
setState(s: Partial<FetchState<TData, TParams>> = {}) {
  this.state = {
    ...this.state,
    ...s,
  };
  // 这里的 subscribe 就是调用 Fetch 时传入的 update 方法
  this.subscribe();
}
ts 复制代码
// 执行所有插件中的 event 事件并收集返回对象,整合后输出
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
  const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);
  return Object.assign({}, ...r);
}

还有 this.countcurrentCount 的逻辑,我们看下 cancel 方法源码:

ts 复制代码
cancel() {
  this.count += 1;
  this.setState({
    loading: false,
  });

  this.runPluginHandler('onCancel');
}

当调用 cancel 方法时,this.count 会加 1。这样this.countcurrentCount 不再相等。

再回过头来看 runAsync 的逻辑就很清晰了

  1. 设置 loading 和 params,触发视图组件更新
  2. 执行 serviceRef 发出异步请求,接受返回结果
  3. 对比 this.countcurrentCount,如果不相等直接返回空 promise,不再执行后续逻辑,这也就实现了一个取消请求的操作,不过要注意的是真实请求依然会继续进行
  4. 设置 loading、data、error,执行 onFinally 插件事件回调

Fetch 里面还有一些其他方法,比如 refreshrefreshAsyncmutate 比较简单,看下源码就能理解。

小结

到此,useRequest 的核心逻辑就读完了,整体分为数据初始化和发送请求两部分。useRequestImplement 通过 hooks 把 react 视图 和 异步数据处理逻辑链接在一起。

下面我们再单独来看 useRequest 的插件和事件订阅是如何实现的。

插件

ts 复制代码
useRequestImplement(service, options, [
  ...(plugins || []),
  useDebouncePlugin,
  useLoadingDelayPlugin,
  usePollingPlugin,
  useRefreshOnWindowFocusPlugin,
  useThrottlePlugin,
  useAutoRunPlugin,
  useCachePlugin,
  useRetryPlugin,
]

我们可以看到,useRequest 以插件形式来实现各种能力,比如防抖、延迟 loading 等等,好处是使核心逻辑更简洁、各功能之间相互解耦可以任意组合。

功能实现

传入到 useRequestImplement 的插件方法会被依次执行,每个插件都会接受到 fetchInstancefetchOptions,可以返回一个对象。

ts 复制代码
// useRequestImplement.ts
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));

插件对象还可以通过 onInit 方法设置初始化 state

ts 复制代码
const fetchInstance = useCreation(() => {
  const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);

  return new Fetch<TData, TParams>(
    serviceRef,
    fetchOptions,
    update,
    Object.assign({}, ...initState),
  );
}, []);

Fetch 中,有两个触发生命周期的方法:

一个是使用者通过 options 参数传入的回调函数,比如

ts 复制代码
const { loading, run } = useRequest(editUsername, {
  onBefore: (params) => {
    message.info(`Start Request: ${params[0]}`);
  },
  onSuccess: (result, params) => {
    message.success(`The username was changed to "${params[0]}" !`);
  },
  onError: (error) => {
    message.error(error.message);
  },
  onFinally: (params, result, error) => {
    message.info(`Request finish`);
  },
});

Fetch 中,在发送异步操作的各个节点执行回调

ts 复制代码
class Fetch<TData, TParams extends any[]> {

  async runAsync(...params: TParams): Promise<TData> {
    // ...

    // ✅ 执行 onBefore 回调
    this.options.onBefore?.(params);

    try {
      const res = await this.serviceRef.current(...params);

      // ✅ 执行 onSuccess 回调
      this.options.onSuccess?.(res, params);
      // ✅ 执行 onFinally 回调
      this.options.onFinally?.(params, res, undefined);
      
      return res;
    } catch (error) {
      // ✅ 执行 onError 回调
      this.options.onError?.(error, params);
      this.options.onFinally?.(params, undefined, error);

      throw error;
    }
  }
}

另一个则是插件方法执行后返回的事件订阅,下面是一个插件的实例代码

ts 复制代码
const useAutoRunPlugin: Plugin<any, any[]> = (
  fetchInstance,
  fetchOptions,
) => {
  useEffect(() => {
    // ...
  }, []);

  return {
    onBefore: () => {
      // ...
    },
    onRequest: () => {},
    onSuccess: () => {}
  };
};

Fetch 源码中,通过 runPluginHandler 方法来执行插件返回的执行 event,看源码:

ts 复制代码
class Fetch<TData, TParams extends any[]> {
  runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
    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;

    // ✅ 执行 onBefore 回调
    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);
    }

    try {
      // replace service
      // ✅ 执行 onRequest 回调
      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(() => {});
      }

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

      // ✅ 执行 onSuccess 回调
      this.runPluginHandler('onSuccess', res, params);

      if (currentCount === this.count) {
        // ✅ 执行 onFinally 回调
        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,
      });

      // ✅ 执行 onError 回调
      this.runPluginHandler('onError', error, params);

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

      throw error;
    }
  }
}

我们可以看到,插件的生命周期事件不仅是获取信息,还可以给 Fetch 回传参数,来控制核心功能里的一些逻辑。

我们找一个插件看下具体逻辑,比如 useAutoRunPlugin.ts 这个插件实现的功能是:

通过设置 options.ready,可以控制请求是否发出。当其值为 false 时,请求永远都不会发出。

其具体行为如下:

  1. manual=false 自动请求模式时,每次 readyfalse 变为 true 时,都会自动发起请求,会带上参数 options.defaultParams
  2. manual=true 手动请求模式时,只要 ready=false,则通过 run/runAsync 触发的请求都不会执行。
ts 复制代码
// useAutoRunPlugin.ts
import { useRef } from 'react';
import useUpdateEffect from '../../../useUpdateEffect';
import type { Plugin } from '../types';

const useAutoRunPlugin: Plugin<any, any[]> = (
  fetchInstance,
  // 使用者传入的 options
  { manual, ready = true, defaultParams = [] },
) => {

  // 插件内部可以使用 react hooks
  useUpdateEffect(() => {
    if (!manual && ready) {
      fetchInstance.run(...defaultParams);
    }
  }, [ready]);

  return {
    // 注册生命周期回调
    onBefore: () => {
      if (!ready) {
        return {
          stopNow: true,
        };
      }
    },
  };
};

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

export default useAutoRunPlugin;

它有几个核心思路:

  1. 因为 useRequest 是一个自定义 hooks,因此这些插件也是自定义 hooks,可以使用 hooks 实现功能
  2. 依赖 ready 的变化,为 true 时,调用 fetchInstance.run 方法执行异步操作
  3. 通过在 onBefore 生命周期中返回 stopNow 来中断请求,我们看下 Fetch 的逻辑,在 runAsync 中获取到 stopNowtrue 时直接返回了空的 Promise
ts 复制代码
async runAsync(...params: TParams): Promise<TData> {
  const {
    stopNow = false,
    returnNow = false,
    ...state
  } = this.runPluginHandler('onBefore', params);

  // stop request
  if (stopNow) {
    return new Promise(() => {});
  }
}
  1. 通过 useAutoRunPlugin.onInit 方法设置 state.loading 对象

小结

其他的插件的实现思路大同小异,都是使用 React hooks 和生命周期函数来实现功能。

大家是否有发现,useRequest 的第三个入参是插件数组,外部可以传入自定义插件,但在使用文档中并没有写,我猜作者是不想让我们传入自定义插件,从源码上看,它的插件实现并没有实现核心与插件的完全解耦,比如 useAutoRunPlugin 返回的 stopNow 参数,在 Fetch 中直接使用了,如果 useAutoRunPlugin 有修改,那 Fetch 核心逻辑也要改动。总不能让大家传入自定义插件的同时,还要提个 PR 去改 Fetch吧,不过对于 useRequest 来说,这种简单的插件设计也够用了。

最后,通过阅读 useRequest 源码,我们对生命周期和插件式编程思想应该都有了入门级的理解,对今后实现自身业务也会有一定帮助。

相关推荐
进取星辰3 分钟前
22、城堡防御工事——React 19 错误边界与监控
开发语言·前端·javascript
海盐泡泡龟1 小时前
ES6新增Set、Map两种数据结构、WeakMap、WeakSet举例说明详细。(含DeepSeek讲解)
前端·数据结构·es6
t_hj2 小时前
Ajax案例
前端·javascript·ajax
bigHead-3 小时前
9. 从《蜀道难》学CSS基础:三种选择器的实战解析
前端·css
阿里小阿希3 小时前
解决 pnpm dev 运行报错的坎坷历程
前端·node.js
未脱发程序员3 小时前
分享一款开源的图片去重软件 ImageContrastTools,基于Electron和hash算法
前端·javascript·electron
视频砖家4 小时前
Web前端VSCode如何解决打开html页面中文乱码的问题(方法2)
前端·vscode·vscode乱码·vscode中文乱码·vscode中文编码
2401_837088504 小时前
CSS transition过渡属性
前端·css
我爱吃朱肉4 小时前
深入理解 CSS Flex 布局:代码实例解析
前端·css
喝养乐多长不高4 小时前
Spring Web MVC基础理论和使用
java·前端·后端·spring·mvc·springmvc