深入 ahooks 3.0 useRequest 源码:插件化架构的精妙设计

深入 ahooks 3.0 useRequest 源码:插件化架构的精妙设计

ahooks 的 useRequest 是一个强大的异步数据管理 Hook,它不仅处理 loading、data、error 等基础状态,还支持轮询、防抖、节流、屏幕聚焦重新请求等高级功能。这一切都建立在一套精妙的插件化架构之上。

一、核心架构:Fetch 类 + Plugin 机制

useRequestImplement.ts 可以看出,核心实现分为三部分:

typescript 复制代码
// 1. 使用 useLatest 保持 service 引用不变
const serviceRef = useLatest(service);

// 2. 使用 useCreation 确保 Fetch 实例只创建一次
const fetchInstance = useCreation(() => {
  const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
  return new Fetch<TData, TParams>(
    serviceRef,
    fetchOptions,
    update,
    Object.assign({}, ...initState),
  );
}, []);

// 3. 运行所有插件钩子
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));

为什么这样做?

  • useLatest:保持函数引用地址不变,但内部始终指向最新的 service 函数
  • useCreation:类似 useMemo,但保证引用稳定,避免 Fetch 实例重复创建
  • 插件化:将非核心功能(防抖、轮询、缓存等)交给插件处理,核心类保持简洁

二、请求竞态问题:count 计数器方案

当用户快速发起多个请求时,可能出现后发起的请求先返回的情况。ahooks 通过 count 计数器解决:

typescript 复制代码
// Fetch 内部实现(简化版)
class Fetch {
  count = 0;  // 请求计数器

  async run(...params) {
    this.count += 1;
    const currentCount = this.count;  // 记录当前请求的 count

    const result = await this.serviceRef.current(...params);

    // 只有最新的请求结果才会被接受
    if (currentCount !== this.count) return;

    this.setState({ data: result });
  }
}

原理 :每次发起请求时 count + 1,请求返回后检查 currentCount === this.count,不匹配则说明已被新请求覆盖,直接丢弃旧结果。

三、组件卸载保护:unmountedRef 标记

避免在组件卸载后执行 setState 导致的内存泄漏警告:

typescript 复制代码
class Fetch {
  unmountedRef = { current: false };

  cancel() {
    this.unmountedRef.current = true;
  }
}

// useRequestImplement.ts 中
useUnmount(() => {
  fetchInstance.cancel();  // 卸载时标记
});

// runAsync 方法中
if (this.unmountedRef.current) return;

通过 unmountedRef 标记位,在请求返回时检查组件是否已卸载,卸载则跳过状态更新。

四、返回方法的引用稳定性:useMemoizedFn

用户可能将 runrefresh 等方法传递给子组件或放入依赖数组,如果引用不稳定会导致无限重渲染:

typescript 复制代码
return {
  run: useMemoizedFn(fetchInstance.run.bind(fetchInstance)),
  refresh: useMemoizedFn(fetchInstance.refresh.bind(fetchInstance)),
  // ...
};

useMemoizedFn 确保无论 Fetch 实例内部如何变化,返回给用户的方法引用始终不变。

五、插件机制的实现

插件通过生命周期钩子介入请求流程,Plugin 类型定义如下:

typescript 复制代码
type Plugin<TData, TParams> = {
  onInit?: (options: Options<TData, TParams>) => any;
  onBefore?: (context: Context<TData, TParams>) => void | Stop;
  onRequest?: (context: Context<TData, TParams>, params: TParams) => void;
  onSuccess?: (data: TData, params: TParams) => void;
  onError?: (error: Error, params: TParams) => void;
  onFinally?: (params: TParams, data?: TData, error?: Error) => void;
  onUnmount?: () => void;
};

runPluginHandler 统一执行插件:

typescript 复制代码
const runPluginHandler = (event: keyof Plugin) => {
  // @ts-ignore
  this.pluginImpls.forEach((impl) => {
    const handler = impl?.[event];
    if (handler) {
      handler(...args);
    }
  });
};

8 个默认插件

ahooks 内置了 8 个插件实现常用功能:

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

每个插件只关注自己的职责,通过生命周期钩子介入请求流程,实现了高度的可扩展性。

总结

ahooks useRequest 的设计精髓在于:

  1. 引用稳定:useLatest、useCreation、useMemoizedFn 三管齐下
  2. 请求安全:count 计数器解决竞态,unmountedRef 防止卸载后更新
  3. 插件化架构:核心类保持简洁,功能扩展通过插件实现

这种设计思想值得在自己的项目中借鉴------核心逻辑稳定可靠,扩展功能灵活可插拔。


参考链接

相关推荐
英俊潇洒美少年11 分钟前
react19和vue3的优缺点 对比
前端·javascript·vue.js·react.js
studyForMokey2 小时前
【Android面试】Activity生命周期专题
android·面试·职场和发展
~无忧花开~2 小时前
React生命周期全解析
开发语言·前端·javascript·react.js·前端框架·react
哈__2 小时前
ReactNative项目OpenHarmony三方库集成实战:react-native-maps
javascript·react native·react.js
SuperEugene3 小时前
Axios + Vue 错误处理规范:中后台项目实战,统一捕获系统 / 业务 / 接口异常|API 与异步请求规范篇
前端·javascript·vue.js·前端框架·axios
行走的陀螺仪3 小时前
手写 Vue3 极简 i18n
前端·javascript·vue.js·国际化·i18n
羽沢313 小时前
一篇简单的STOMP教程QAQ
前端·javascript·stomp
Kel3 小时前
深入 OpenAI Node SDK:一个请求的奇幻漂流
javascript·人工智能·架构
子兮曰3 小时前
AI写代码坑了90%程序员!这5个致命bug,上线就炸(附避坑清单)
前端·javascript·后端
终端鹿4 小时前
Vue3 核心 API 补充解析:toRef / toRefs / unref / isRef
前端·javascript·vue.js