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

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


参考链接

相关推荐
电商API_180079052472 小时前
淘宝商品详情数据获取全方案分享
开发语言·前端·javascript
前端不开发2 小时前
在任意网页里“召唤”一个火柴人:一次有趣的 JavaScript Hack
前端·javascript
maplewen.3 小时前
C++11 返回值优化
开发语言·c++·面试
UrbanJazzerati3 小时前
从零到一:用Python Tkinter打造专业的文件行删除工具(一)
后端·面试
icebreaker4 小时前
Mokup:构建工具友好的可视化 Mock 工具
前端·javascript·后端
禾叙_4 小时前
【netty】Channel
开发语言·javascript·ecmascript
不想秃头的程序员4 小时前
Vue3 子传父全解析:从基础用法到实战避坑
前端·vue.js·面试
简单Janeee5 小时前
[Vue 3 从零到上线]-第三篇:网页的指挥官——指令系统 (v-if, v-for, v-bind, v-on)
前端·javascript·vue.js
2501_901147835 小时前
幂函数实现的优化与工程思考笔记
笔记·算法·面试·职场和发展·php