ahooks/useRequest
源码解读- 文件构造
收口处 ·
useRequest.ts
- 源码(
useRequest.ts
) - 整体解读(
useRequest.ts
)
- 源码(
出入参加实例化 ·
useRequestImplement.ts
- 源码(
useRequestImplement.ts
) - 外围实例 ·
Fetch
实例fetchInstance
- 入参配置
fetchInstance
的生命周期治理- 出参解决
- 源码(
幕后大佬
Fetch.ts
- 源码(
Fetch.ts
) - 整体解读(
Fetch.ts
) - 结构 ·
constructor
- 插件解决 ·
runPluginHandler
外围办法 ·
run
&runAsync
- step1, 插件
onBefore
, 判断是否立刻进行 - step2, 判断是否立刻返回
- step3, 用户
onBefore
step4, 真正的申请——
try{}catch{}
try
的局部catch
的局部
- 对于
runAsync
, 小叨几句
- step1, 插件
- 勾销 ·
cancel
- 更新 ·
refresh
&refreshAsync
mutate
- 源码(
ahooks/useRequest
源码解读
useRequest
作为ahooks
中最重要的成员之一, 在理论开发中的应用频率还是很高的, 性能也很全面, 值得一读.
文件构造
useRequest
外围代码位于我的项目packages/hooks/src/useRequest/src/
, 其下文件目录:
src├── Fetch.ts├── types.ts├── useRequest.ts├── useRequestImplement.ts├── plugins│ ├── useAutoRunPlugin.ts│ ├── useCachePlugin.ts│ ├── useDebouncePlugin.ts│ ├── useLoadingDelayPlugin.ts│ ├── usePollingPlugin.ts│ ├── useRefreshOnWindowFocusPlugin.ts│ ├── useRetryPlugin.ts│ └── useThrottlePlugin.ts└── utils ├── cache.ts ├── cachePromise.ts ├── cacheSubscribe.ts ├── isDocumentVisible.ts ├── isOnline.ts ├── limit.ts ├── subscribeFocus.ts └── subscribeReVisible.ts
收口处 · useRequest.ts
自顶向下的看, 天然是从useRequest.ts入
手, 但显然, 这只是一个暗藏了外围逻辑的规范化收口文件 ⬇️
源码(useRequest.ts
)
import useAutoRunPlugin from './plugins/useAutoRunPlugin';import useCachePlugin from './plugins/useCachePlugin';import useDebouncePlugin from './plugins/useDebouncePlugin';import useLoadingDelayPlugin from './plugins/useLoadingDelayPlugin';import usePollingPlugin from './plugins/usePollingPlugin';import useRefreshOnWindowFocusPlugin from './plugins/useRefreshOnWindowFocusPlugin';import useRetryPlugin from './plugins/useRetryPlugin';import useThrottlePlugin from './plugins/useThrottlePlugin';import type { Options, Plugin, Service } from './types';import useRequestImplement from './useRequestImplement';function useRequest<TData, TParams extends any[]>( service: Service<TData, TParams>, options?: Options<TData, TParams>, plugins?: Plugin<TData, TParams>[],) { return useRequestImplement<TData, TParams>(service, options, [ ...(plugins || []), useDebouncePlugin, useLoadingDelayPlugin, usePollingPlugin, useRefreshOnWindowFocusPlugin, useThrottlePlugin, useAutoRunPlugin, useCachePlugin, useRetryPlugin, ] as Plugin<TData, TParams>[]);}export default useRequest;
整体解读(useRequest.ts
)
抛开import
、type
, 这三十行代码实际上只有一句话:
function useRequest(service, options, plugins) { return useRequestImplement(service, options, [ ...(plugins || []), // 默认插件们 ];}export default useRequest;
可见useRequest
办法理论就只是返回了它隔壁useRequestImplement
, 同时多带了一些默认插件. 插件具体性能暂且按下不表, 还是先看看更重要的useRequestImplement.ts
.
出入参加实例化 · useRequestImplement.ts
先上源码.
源码(useRequestImplement.ts
)
import useCreation from '../../useCreation';import useLatest from '../../useLatest';import useMemoizedFn from '../../useMemoizedFn';import useMount from '../../useMount';import useUnmount from '../../useUnmount';import useUpdate from '../../useUpdate';import Fetch from './Fetch';import type { Options, Plugin, Result, Service } from './types';function useRequestImplement<TData, TParams extends any[]>( service: Service<TData, TParams>, options: Options<TData, TParams> = {}, plugins: Plugin<TData, TParams>[] = [],) { const { manual = false, ...rest } = options; const fetchOptions = { manual, ...rest, }; const serviceRef = useLatest(service); const update = useUpdate(); const fetchInstance = useCreation(() => { const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean); return new Fetch<TData, TParams>( serviceRef, fetchOptions, update, Object.assign({}, ...initState), ); }, []); fetchInstance.options = fetchOptions; // run all plugins hooks fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions)); useMount(() => { if (!manual) { // useCachePlugin can set fetchInstance.state.params from cache when init const params = fetchInstance.state.params || options.defaultParams || []; // @ts-ignore fetchInstance.run(...params); } }); useUnmount(() => { fetchInstance.cancel(); }); return { loading: fetchInstance.state.loading, data: fetchInstance.state.data, error: fetchInstance.state.error, params: fetchInstance.state.params || [], cancel: useMemoizedFn(fetchInstance.cancel.bind(fetchInstance)), refresh: useMemoizedFn(fetchInstance.refresh.bind(fetchInstance)), refreshAsync: useMemoizedFn(fetchInstance.refreshAsync.bind(fetchInstance)), run: useMemoizedFn(fetchInstance.run.bind(fetchInstance)), runAsync: useMemoizedFn(fetchInstance.runAsync.bind(fetchInstance)), mutate: useMemoizedFn(fetchInstance.mutate.bind(fetchInstance)), } as Result<TData, TParams>;}export default useRequestImplement;
外围实例 · Fetch
实例fetchInstance
可见, useRequestImplement
中的性能为:
应用useCreation
实例化一个Fetch
类: fetchInstance
, 并对出入参配置及Fetch
实例生命周期相干逻辑进行简略解决. 由此已能够推知外围逻辑该当在Fetch.ts
中.
实例化代码:
const fetchInstance = useCreation(() => { const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean); return new Fetch<TData, TParams>( // new一个实例 serviceRef, fetchOptions, update, Object.assign({}, ...initState), ); }, []);
而其余代码都围绕这一实例开展:
入参配置
入参, 配置实例. useRequestImplement
组件共三项入参: service
, options
, plugins
.
- 将组件入参中的
service
装进useLatest
1以总是调用最新值, 并将返回的serviceRef
作为入参在实例化Fetch
时传入 - 实例化后, 配置
fetchInstance
的options
为组件入参中的options
, 同时默认设置manul
为false
- 实例化后, 配置
fetchInstance
的pluginImpls
为组件入参中的plugins
, 并让所有插件跑起来
fetchInstance
的生命周期治理
实例化后, 生命周期治理
- 在
useMount
中, 如果manul = false
即非手动调用, 则在此调用fetchInstance.run(...params)
唤醒fetachInstance
, 发动申请 在
useUnmount
中调用fetchInstance.cancel()
, 进行可能有未实现的申请出参解决
出参. 所有出参都从
FetchInstance
中返回, 具体能够分为返回值和办法.- 返回值: 如
loading
,data
,error
,params
. 间接从fetchInstance.state
中获取并返回. - 六个办法: 有
cancel
,refresh
,refreshAsync
,run
,runAsync
,mutate
. 则为fn: useMemoizedFn(fetchInstance.fn.bind(fetchInstance))
, 应用class
的写法绑定后通过useMemoizedFn
2返回以确保函数地址不变.
至此, useRequestImplement.ts
的次要内容就读完了, 接下来进入真正的拉取申请环节——Fetch
.
幕后大佬 Fetch.ts
useRequest.ts
, useRequestImplement.ts
, Fetch.ts
三个文件别离有30、70和170行…… 因而…… 不想看的就往下多滑两下, 前面还会逐段粘贴.
源码(Fetch.ts
)
/* eslint-disable @typescript-eslint/no-parameter-properties */import { isFunction } from '../../utils';import type { MutableRefObject } from 'react';import type { FetchState, Options, PluginReturn, Service, Subscribe } from './types';export default class Fetch<TData, TParams extends any[]> { pluginImpls: PluginReturn<TData, TParams>[]; count: number = 0; state: FetchState<TData, TParams> = { loading: false, params: undefined, data: undefined, error: undefined, }; constructor( public serviceRef: MutableRefObject<Service<TData, TParams>>, public options: Options<TData, TParams>, public subscribe: Subscribe, public initState: Partial<FetchState<TData, TParams>> = {}, ) { this.state = { ...this.state, loading: !options.manual, ...initState, }; } setState(s: Partial<FetchState<TData, TParams>> = {}) { this.state = { ...this.state, ...s, }; this.subscribe(); } runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) { // @ts-ignore const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean); return Object.assign({}, ...r); } async runAsync(...params: TParams): Promise<TData> { 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; } } run(...params: TParams) { this.runAsync(...params).catch((error) => { if (!this.options.onError) { console.error(error); } }); } cancel() { this.count += 1; this.setState({ loading: false, }); this.runPluginHandler('onCancel'); } refresh() { // @ts-ignore this.run(...(this.state.params || [])); } refreshAsync() { // @ts-ignore return this.runAsync(...(this.state.params || [])); } mutate(data?: TData | ((oldData?: TData) => TData | undefined)) { let targetData: TData | undefined; if (isFunction(data)) { // @ts-ignore targetData = data(this.state.data); } else { targetData = data; } this.runPluginHandler('onMutate', targetData); this.setState({ data: targetData, }); }}
整体解读(Fetch.ts
)
首先是一个好消息:
import { isFunction } from '../../utils';import type { MutableRefObject } from 'react';import type { FetchState, Options, PluginReturn, Service, Subscribe } from './types';
Fetch.ts
的所有import
都在这里 ⬆️ , 显然这就是ahooks/useRequest
的最里一层套娃了, 也是外围代码所在.
值得一提的是, 这和前两个文件不同, 是用class
而非function
. 浏览整个类的构造, 能够说是相当的清晰.
- 状态治理: 几个很好了解的
state
和setState
函数, 不另外解读了 - 构造方法:
constructor
- 惟一的辅助工具(插件解决):
runPluginHandler
- 外围局部(申请解决):
async
,runAsync
- 上文六个返回办法中, 除
async
,runAsync
外的五个
接来下, 就从constructor
开始浏览.
结构 · constructor
constructor( public serviceRef: MutableRefObject<Service<TData, TParams>>, // 第一个 public options: Options<TData, TParams>, // 第二个 public subscribe: Subscribe, // 第三个 public initState: Partial<FetchState<TData, TParams>> = {}, // 第四个 ) { this.state = { ...this.state, // 原来的参数不变 loading: !options.manual, // 如果非手动触发, 那么当初就开始loading了 ...initState, // 存入插件的初始化参数 }; }
⬆️ 下面四个入参对应useRequestImplement
中实例化时的四个入参 ⬇️
const fetchInstance = useCreation(() => { const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean); // 捞一捞插件的初始化参数 return new Fetch<TData, TParams>( serviceRef, // 第一个 fetchOptions, // 第二个 update, // 第三个 Object.assign({}, ...initState), // 第四个 ); }, []);
整体来看, 四个入参都被间接作为了public
成员, 因此如serviceRef
, options
中manual
以外的值, 尽管在constructor
中没有做解决或保留, 但其实曾经成为了Fetch
的公共成员, 能够在其余成员函数中通过this
间接调用.
其实真正做的解决, 只有一个loading
状态依据是否手动参数manual
更新. 其余initState
都是插件的初始化参数.
插件解决 · runPluginHandler
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) { // @ts-ignore const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean); return Object.assign({}, ...r); }
这一办法将遍历pluginImpls
所有插件, 执行其event
指定的事件办法, 遍历后合并返回所有插件的返回.
外围办法 · run
& runAsync
尽管run
看起来也很外围, 但实际上不论是run
还是runAsync
, 理论都是调用runAsync
, 看看run
的源码就晓得了 ⬇️
// run run(...params: TParams) { this.runAsync(...params).catch((error) => { if (!this.options.onError) { console.error(error); } }); }
所以还是来看runAsync
⬇️
// runAsync async runAsync(...params: TParams): Promise<TData> { 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; } }
有点长, 须要分几个局部来读.
step1, 插件onBefore
, 判断是否立刻进行
this.count += 1; // 设置计数器 + 1 const currentCount = this.count; // 设置以后是几号申请 const { stopNow = false, returnNow = false, ...state } = this.runPluginHandler('onBefore', params); // stop request if (stopNow) { return new Promise(() => {}); }
- 此时首先解决了
count
, 用于辨别每一次申请, 即哪怕没开始就进行了, 也要算作一次申请, 防止凌乱. - 第二步, 运行插件的
onBefore
办法, 更新办法中的状态state
并捞出stopNow
,returnNow
备用. - 判断是否
stopNow
, 如果是, 那么就返回空对象, 申请完结了.
step2, 判断是否立刻返回
this.setState({ loading: true, params, ...state, }); // return now if (returnNow) { return Promise.resolve(state.data); }
- 更新
Fetch
类的状态state
, 其中loading: true
, 申请开始, 同时将申请参数放入类状态中. - 判断上一部分捞回的
returnNow
, 如果是, 那么就立刻返回办法中的状态中的data
, 申请完结了.
step3, 用户onBefore
运行useRequest
中用户入参options
中传入的onBefore
办法.
this.options.onBefore?.(params);
step4, 真正的申请——try{}catch{}
留神, 这一部分所有内容都被包在try{}catch{}
中.
try
的局部
此时开始进入onRequest
状态了, 意味着要调用所有插件中的onRequest
办法更新服务, 而后发动申请 ⬇️
// replace service let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params); if (!servicePromise) { servicePromise = this.serviceRef.current(...params); } const res = await servicePromise; // 发动申请!!!
此处真正的作用是更新service
, 如果插件有返回新的服务, 则调用新的服务, 否则才应用本来useRequestImplement
中的服务.
申请完了…… 然而如果此时申请曾经被过期(即被勾销或笼罩), 那么就只好返回空对象了 ⬇️
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, });
有一个被正文掉了的format
(?), 而后将返回后果赋值给data
, 同时设置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;
按options.onSuccess
, 所有插件的onSuccess
, options
. onFinally
, 所有插件的onFinally
顺次调用. 当然, 如果以后申请曾经过期, 那么最初一项插件的onFinally
事件则不会再调用.
至此, 就是一切顺利, 返回res
啦.
catch
的局部
既然出问题了…… 那么先来判断一把申请是不是曾经过期了. be like: pre曾经完结了, ppt就算炸了也没有关系.
if (currentCount !== this.count) { // prevent run.then when request is canceled return new Promise(() => {}); }
如果申请曾经过期, 那么返回空对象即可.
否则, 只能用loading: false
宣告申请完结, 而后将error
存入类状态中. ⬇️
this.setState({ error, loading: false, });
最初, 按和try
结尾截然不同的逻辑顺次调用onError
, onFinally
.
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;
最初抛出error
(暗爽)!
对于runAsync
, 小叨几句
runAsync
的句子比拟长, 但总体而言内容不多, 无非几步: onBefore
, 查看是否进行, 查看是否立刻返回, try
申请…… 申请完, 查看申请是否过期, onSuccess / onError
, onFinally
.
其中有四个on
结尾的事件, 而每到相应事件阶段, 都会别离调用用户从useRequest
的options
中传入的事件办法 和 所有插件中的事件办法. 从中不难留神到, 申请前的onBefore
事件时是先遍历调用插件中的事件后调用用户传入的事件, 申请后的三个事件反之. 集体对此其实不太理解, 浅显的了解了一下, 认为用户的事件在此总是比插件间隔申请自身更近, 即用户具备最实在资料(即申请自身)的最终决定和第一个应用的权力, 插件的权限次之. 将事件跨度再拉大一些, 其实也是让用户的事件办法间隔useRequest
的出入参更远——试想, 刚拿到入参就做onBefore
解决, 为什么不先解决了再给入参呢?
好了, 外围申请逻辑都在下面. 接下来看看返回给用户的六个办法. 当然, 虽说有六个, 实则接下来要聊的只有四个, 因为其中run
和runAsync
曾经读过了.
勾销 · cancel
cancel() { this.count += 1; this.setState({ loading: false, }); this.runPluginHandler('onCancel'); }
这里逻辑很简略, 计数器 + 1
, loading: false
就强制将申请置为进行状态了, 当真正申请回来时比照计数器, 就会发现曾经过期了~
更新 · refresh
& refreshAsync
refresh() { // @ts-ignore this.run(...(this.state.params || [])); } refreshAsync() { // @ts-ignore return this.runAsync(...(this.state.params || [])); }
其实也就是从新run
啦.
mutate
mutate(data?: TData | ((oldData?: TData) => TData | undefined)) { let targetData: TData | undefined; if (isFunction(data)) { // @ts-ignore targetData = data(this.state.data); } else { targetData = data; } this.runPluginHandler('onMutate', targetData); this.setState({ data: targetData, }); }
mutate
用于手动批改data
. 这一段逻辑也比较简单, 共三小步:
- 判断入参
data
是否是办法后进行调用或复制 - 遍历调用插件中的
onMutate
办法 - 更新类状态中的
data
为最新的值
目前, useRequest
中的外围源码曾经读完了, 能看到都是十分外围的功能模块, 而许多实用个性理论都在插件中. 这篇临时不做开展, 下次肯定.
20220731
次日更新:
✨Start
安顿, 插件马上就写.
first published on 知乎专栏, 20220730
.
本文git地址, 欢送✨Start
: https://github.com/qiaork/fro...
- useLatest: ahooks 中与 useRequest 同一目录层级的 hook, 封装了 useRef. 返回以后最新值的 Hook, 能够防止闭包问题. https://ahooks.js.org/zh-CN/h... ↩
- useMemoizedFn: ahooks 中与 useRequest 同一目录层级的 hook, 基于 useMemo 和 useRef 进行封装. 长久化 function 的 Hook, 实践上, 能够应用 useMemoizedFn 齐全代替 useCallback. 能够省略第二个参数 deps, 同时保障函数地址永远不会变动. https://ahooks.js.org/zh-CN/h... ↩