深入 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. 插件化架构:核心类保持简洁,功能扩展通过插件实现

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


参考链接

相关推荐
三十而立洋1 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
晨米酱1 天前
AGENTS.md:Agent 的上下文策略层
面试·架构·agent
卡布鲁1 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
李少兄1 天前
JavaScript 隐式全局变量解析
javascript
彧azz1 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
沙蒿同学1 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端
CoderYanger1 天前
A.每日一题:835. 图像重叠
java·开发语言·程序人生·leetcode·面试·职场和发展·学习方法
paopaokaka_luck1 天前
智慧社区综合服务小程序(人脸识别、AI问答、Echarts图形化分析)
前端·javascript·spring boot·spring·数据分析·echarts
爱学习的执念1 天前
助力金九银十1000道软件测试面试题(功能、接口、自动化、WEB、APP.......)附答案
软件测试·面试
小的~~1 天前
银河麒麟V10 ARM部署TDengine(涛思)工业时序数据库安装与测试
面试·程序员创富