深入 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
用户可能将 run、refresh 等方法传递给子组件或放入依赖数组,如果引用不稳定会导致无限重渲染:
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 的设计精髓在于:
- 引用稳定:useLatest、useCreation、useMemoizedFn 三管齐下
- 请求安全:count 计数器解决竞态,unmountedRef 防止卸载后更新
- 插件化架构:核心类保持简洁,功能扩展通过插件实现
这种设计思想值得在自己的项目中借鉴------核心逻辑稳定可靠,扩展功能灵活可插拔。
参考链接: