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 源码,我们对生命周期和插件式编程思想应该都有了入门级的理解,对今后实现自身业务也会有一定帮助。

相关推荐
纽格立科技7 分钟前
DRM 发射端链路图(上)
前端·人工智能·车载系统·信息与通信·传媒
云水一下19 分钟前
Vue.js从零到精通系列(七):高级特性实战——Teleport、异步组件、自定义指令与TypeScript深度结合
前端·vue.js·typescript
qq43569470122 分钟前
Vue05
前端·vue.js
qq_4221525724 分钟前
PDF 解密工具怎么选?2026 年文档密码移除方案与注意事项
java·前端·pdf
YHHLAI27 分钟前
前端工程化调用 AI 多模态生图模型:Qwen Image Demo 实战
前端·人工智能
To_OC41 分钟前
我一直以为 Ajax 是个黑盒,直到我写了这 50 行代码
前端·后端·全栈
用户059540174461 小时前
RAG 记忆层踩坑实录:用户偏好凭空消失,我排查了 4 小时,最后用 LangChain + Chroma 搭了套自动化回归测试
前端·css
程序猿阿伟1 小时前
《Chrome隔离机制的维度落地指南》
前端·chrome
用户054324329701 小时前
AI 生成的代码怎么在前端安全预览 + 一键运行:sandbox iframe 实战 🔒
前端
ALianBlank1 小时前
一个 Unity 框架能做多少事?86 个模块 + 21 个小游戏平台
前端·后端·游戏开发