您现在的位置是:网站首页> 编程资料编程资料
ahooks useRequest源码精读解析_React_
2023-05-24
411人已围观
简介 ahooks useRequest源码精读解析_React_
前言
自从 React v16.8 推出了 Hooks API,前端框架圈并开启了新的逻辑复用的时代,不再需要在意 HOC 的无限套娃导致性能差的问题,也解决了 mixin 的可阅读性差的问题。当然对于 React 最大的变化是函数式组件可以有自己的状态,扁平化的逻辑组织方式,更加友好地支持 TS 类型声明。
除了 React 官方提供的一些 Hooks,也支持我们能根据自己的业务场景自定义 Hooks,还有一些通用的 Hooks,例如用于请求的 useRequest,用于定时器的 useTimeout,用于节流的 useThrottle 等。于是出现了大量的 Hooks 库,ahooks 是其中比较受欢迎的 Hooks 库之一,其提供了大量的 Hooks,基本满足了大多数场景的需求。又是国人开发,中文文档友好,在我们团队的一些项目中就使用了 ahooks。
其中最常用的 hooks 就是 useRequest,用于从后端请求数据的业务场景,除了简单的数据请求,它还支持:
- 轮询
- 防抖和节流
- 错误重试
- SWR(stale-while-revalidate)
- 缓存
等功能,基本上满足了我们请求后端数据需要考虑的大多数场景,当然还有 loading-delay、页面 foucs 重新刷新数据等这些功能,但是个人理解上面列的功能才是使用比较频繁的功能点。
一个 Hooks 实现这么多功能,我还是对其内部的实现比较好奇的,所以本文就从源码的角度带大家了解 useRequest 的实现。
架构图
我们从一张图开始了解其模块设计,对于一个功能复杂的 API,如果不使用合适的架构和方式组织代码,其扩展性和可维护性肯定比较差。功能点实现和核心代码混在一起,阅读代码的人也无从下手,也带来更大的测试难度。虽然 useRequest 只是一个 Hook,但是实际上其设计还是有清晰的架构,我们来看看 useRequest 的架构图:

我把 useRequest 的模块划分为三大块:Core、Plugins、utils,然后 useRequest 将这些模块组合在一起实现核心功能。
先看插件部分,看到每个插件的命名,如果了解 useRequest 的功能就会发现,基本上每个功能点对应一个插件。这也是 useRequest 设计比较巧妙的一点,通过插件化机制降低了每个功能之间的耦合度,也降低了其本身的复杂度。这些点我们在分析具体的源码的时候会再详细介绍。
另外一部分核心的代码我将其归类为 Core(在 useRequest 的源码中没有这个名词),主要实现了一个 Fetch 类,这个类是 useRequest 的插件化机制实现和其它功能的核心实现。
下面我们深入源码,看下其实现原理。
源码解析
先看 Core 部分的源码,主要是 Fetch 这个类的实现。
Fetch
先贴代码:
export default class Fetch{ pluginImpls: PluginReturn []; count: number = 0; state: FetchState = { loading: false, params: undefined, data: undefined, error: undefined, }; constructor( public serviceRef: MutableRefObject >, public options: Options , public subscribe: Subscribe, public initState: Partial > = {}, ) { this.state = { ...this.state, loading: !options.manual, ...initState, }; } setState(s: Partial > = {}) { // 省略一些代码 } runPluginHandler(event: keyof PluginReturn , ...rest: any[]) { // 省略一些代码 } async runAsync(...params: TParams): Promise { // 省略一些代码 } run(...params: TParams) { // 省略一些代码 } cancel() { // 省略一些代码 } refresh() { // 省略一些代码 } refreshAsync() { // 省略一些代码 } mutate(data?: TData | ((oldData?: TData) => TData | undefined)) { // 省略一些代码 } }
Fetch 类 API 的设计还是比较简洁的,而且也不是特别多,实际上有些 API 就是直接从 useRequest 暴露给外部用户使用的,比如 run、runAsync、cancel、refresh、refreshAsync、mutate 等。像 runPluginHandler、setState 等 API 主要是给内部用的 API,不过它也没有做区分,从封装的角度上来说,这一点个人感觉设计得不够好。
重点关注下几个 Fetch 类的属性,一个是 state,它的类型是 FetchState,一个是 pluginImpls,它是 PluginReturn 数组,实际上这个属性就用来存所有插件执行后返回的结果。还有一个 count 属性,是 number 类型,不看具体源码,完全不知道这个属性是做什么用的。这点也是 useRequest 开发者做得感觉不是很好的地方,很少有注释,纯靠阅读者深入到源码,去看使用的地方,才能知道一些方法和属性的作用。
那我们先来看下 FetchState 的定义,它定义在 src/type.ts 里面:
export interface FetchState{ loading: boolean; params?: TParams; data?: TData; error?: Error; }
它的定义还是比较简单,看起来是存一个请求结果的上下文信息,这些信息其实都是需要暴露给外部用户的,例如 loading、data、errors 等不就是我们使用 useRequest 经常需要拿到的数据信息:
const { data, error, loading } = useRequest(service); 而对应的 Fetch 封装了 setState API,实际上就是用来更新 state 的数据:
setState(s: Partial> = {}) { this.state = { ...this.state, ...s, }; // ? 未知 this.subscribe(); }
除了更新 state,这里还调用了一个 subscribe 方法,这是初始化 Fetch 类的时候传进来的一个参数,它的类型是 Subscribe,等后面将到调用的地方再看这个方法是怎么实现的,以及它的作用。
再看下 PluginReturn 的类型定义:
export interface PluginReturn{ onBefore?: (params: TParams) => | ({ stopNow?: boolean; returnNow?: boolean; } & Partial >) | void; onRequest?: ( service: Service , params: TParams, ) => { servicePromise?: Promise ; }; onSuccess?: (data: TData, params: TParams) => void; onError?: (e: Error, params: TParams) => void; onFinally?: (params: TParams, data?: TData, e?: Error) => void; onCancel?: () => void; onMutate?: (data: TData) => void; }
实际上都是一些回调钩子,从名字对应上来看,对应了请求的各个阶段,除了 onMutate 是其内部扩展的一个钩子。
也就是说 pluginImpls 里面存的是一堆含有各个钩子函数的对象集合,如果技术敏锐的同学,可能很容易就想到发布订阅模式,这不就是存了一系列的 subscribe 回调,这不过这是一个回调的集合,里面有各种不同请求阶段的回调。那么到底是不是这样,我们继续往下看。
要搞清楚 Fetch 的运作方式,我们需要看两个核心 API 的实现:runPluginHandler 和 runAsync,其它所有的 API 实际上都在调用这两个 API,然后做一些额外的特殊逻辑处理。
先看 runPluginHandler:
runPluginHandler(event: keyof PluginReturn, ...rest: any[]) { // @ts-ignore const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean); return Object.assign({}, ...r); }
这个方法实现还是比较简单,只有两行代码。跟我们之前猜测的大致差不多,这个方法就是接收一个 event 参数,它的类型就是 keyof PluginReturn,也就是:onBefore | onRequest | onSuccess | onError | onFinally | onCancel | onMutate 的联合类型,以及其它额外的参数,然后从 pluginImpls 中找出所有对应的 event 回调钩子函数,然后执行回调函数,拿到结果并返回。
再看 runAsync 的实现:
async runAsync(...params: TParams): Promise{ this.count += 1; const currentCount = this.count; 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); } this.options.onBefore?.(params); try { // replace service 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(() => {}); } // const formattedResult = this.options.formatResultRef.current ? this.options.formatResultRef.current(res) : res; this.setState({ data: res, error: undefined, loading: false, }); this.options.onSuccess?.(res, params); this.runPluginHandler('onSuccess', res, params); this.options.onFinally?.(params, res, undefined); 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, }); this.options.onError?.(error, params); this.runPluginHandler('onError', error, params); this.options.onFinally?.(params, undefined, error); if (currentCount === this.count) { this.runPluginHandler('onFinally', params, undefined, error); } throw error; } }
看着代码挺多的,其实看下来很好理解。 这个函数实际上做的事就是调用我们传入的获取数据的方法,然后拿到成功或者失败的结果,进行一系列的数据处理,然后更新到 state,执行插件的各回调钩子,还有就是我们通过 options 传入的回调函数。
可能直接用文字直接描述比较抽象,下面我们分请求阶段分析代码。
首先前两行是对 count 属性的累加处理,之前我们不知道这个属性的作用,看到这里可能猜测大概是跟请求相关的,后面看到 currentCount 的使用的地方,我们再说。
onBefore
接下来 5~27 行实际上是对 onBefore 回调钩子的执行,然后拿到结果做的一些逻辑处理。这里调用的就是 runPluginHandler 方法,传入的参数是 onBefore 和外部用户定义的 params 参数。然后执行完所有的 onBefore 钩子函数,拿到最后的结果,如果 stopNow 的 flag 是 true,则直接返回没有结果的 Promise。看注释,我们知道这里实际上做的是取消请求的处理,当我们在 onBefore 的钩子里实现了取消的逻辑,符合条件后并会真正的阻断请求。
如果没有取消,然后接着更新 state 数据,如果立即返回的 returnNow flag 为 true,则立马将更新后的 state 返回,否则执行用户传入的 options 中的 onBefore 回调,也就是说在调用 useRequest 的时候,我们可以通过 options 参数传入 onBefore 函数,进行请求之前的一些逻辑处理。
onRequest
接下来后面的代码就是真正执行请求数据的方法了,这里就会执行所有的 onRequest 钩子。实际上,通过 onRequest 钩子我们是可以重写传入的获取数据的方法,因为最后执行的是 onRequest 回调返回的 servicePromise。
拿到最后执行的请求数据方法,就开始发起请求。在这里发现了前面的 currentCount 的使用,它会去对比当前最新的 count 和执行这个方法时定义的 currentCount 是否相等,如果不相等,则会做类似于取消请求的处理。这里大概知道 count 的作用类似于一个”锁“的作用,我的理解是,如果在执行这些代码过程有产生一些比这里优先级更高的处理逻辑或者请求操作,是需要 cancel 掉这次的请求,以最新的请求为准。当然,最后还是要看哪些地方可能会修改 count。
onSuccess
执行完请求后,如果请求成功,则拿到请求返回的数据,更新到 state,执行用户传入的成功回调和各插件的成功回调钩子。
onFinally
成功之后,执行 onFinally 钩子,这里也很严谨,也会比较 count 的值,确保一致之后,才会执行各插件的回调钩子,预发
相关内容
- 插件化机制优雅封装你的hook请求使用方式_React_
- 微信小程序复选框组件使用详解_javascript技巧_
- ahooks整体架构及React工具库源码解读_React_
- JavaScript中异步与回调的基本概念及回调地狱现象_javascript技巧_
- 微信小程序实现日期时间筛选器_javascript技巧_
- 微信小程序实现地区选择伪五级联动_javascript技巧_
- 微信小程序自定义多列选择器使用_javascript技巧_
- useEffect中不能使用async原理详解_React_
- 微信小程序多项选择器checkbox_javascript技巧_
- React useEffect不支持async function示例分析_React_
