源码地址

  1. 文档:https://ahooks.gitee.io/zh-CN/hooks/use-request/index
  2. GIT:https://github.com/alibaba/hooks/tree/master/packages/hooks/src/useRequest

先搞清楚几个点

为什么需要

回忆我们最常见的业务场景,列表展示。 进入页面,在页面初始化钩子内,调用后端接口拉取数据,考虑到用户体验问题会分为几个状态去展示:

  1. 拉取接口时,给到一个 loading icon 展示,记录一个 loading true / false 的值
  2. 成功拉取数据情况,正常的列表展示,记录一个 data 的数据
  3. 接口炸了等失败情况,展示错误提示 tips,记录一个 error 的数据

模版代码往往类似是这样的:

  1. import sampleApi from '???'
  2. import { useEffect, useState } from 'react'
  3. const Home = () => {
  4. const [loading, setLoading] = useState(false)
  5. const [error, setError] = useState()
  6. const [data, setData] = useState()
  7. const init = async () => {
  8. setLoading(true)
  9. try {
  10. const result = await sampleApi()
  11. setData(result)
  12. } catch (error) {
  13. setError(error)
  14. } finally {
  15. setLoading(false)
  16. }
  17. }
  18. useEffect(() => { init() }, [])
  19. if (loading) return 'loading'
  20. if (error) return 'sorry, error'
  21. return <div>{JSON.stringify(data)}</div>
  22. }
  23. export default Home

大致所有页面交互都离不开这样的过程,自然就会想到过程封装:

  1. import sampleApi from '???'
  2. const Home = () => {
  3. const { loading, error, data } = useRequest(sampleApi)
  4. if (loading) return 'loading'
  5. if (error) return 'sorry, error'
  6. return <div>{JSON.stringify(data)}</div>
  7. }
  8. export default Home

代码量从原来 15 行的关键逻辑变为 1行,若有 100 个类似的过程,也就减少约了 (15-1) X 100 = 1400 行代码,因此这是针对重复逻辑的封装,减少代码量

不仅如此

再来回忆高级一点点的场景。

  1. 这个列表有实时性展示的要求,需要每 10s 中请求一次,展示到最新的数据(轮询)
  2. 在连续请求的场景内,希望只触发到最后一次(防抖)
  3. 在连续请求的场景内,希望一段时间内,只触发一次(节流)

因此不仅仅是针对一个异步操作过程的简单封装,甚至可以丰富更多的功能,比如 useRequest 提供的:截屏2022-04-15 下午3.47.07.png

与具体请求库无关

只要求返回 Promise 函数都可使用,因此可以根据个人喜好,在项目内集成对应的请求库

  1. export type Service<TData, TParams extends any[]> = (...args: TParams) => Promise<TData>;

整体架构

截屏2022-04-21 11.04.05.png

  • 暴露对外的 props / methods:蓝色
  • 插件 / 插件暴露钩子:红色
  • Class Fetch:黄绿色
  • useRequest:灰黑色

    1. Main 流程

  • initState:遍历调用 plugins 暴露的 onInit 方法,获取到 initState,并对此进行合并

  • 实例化 Fetch:把 props 与上步获取的 initState 传至 Fetch 进行实例化
  • 插件执行&注册:遍历调用 plugins hooks 方法,并把 hooks 保存至实例化后的 fetchInstance.pluginImpls
  • 第一次加载时:判断 manual(是否手工触发)是否为 false,若是,则调用 fetchInstance.run 方法进行执行异步过程
  • hooks 卸载:调用 fetchInstance.cancel 方法进行销毁

    2. Class Fetch

    异步数据管理器,维护了一个异步过程,定义了相关的生命周期。并且管理了所有的功能插件 hooks。

    3. 插件式代码组织

    useRequest 内除基础功能以外的附加功能,都是以一个个小插件的方式进行管理的(如图红色标注部分,为插件管理的逻辑),然而每一个插件功能,又是一个 hooks:

    目录结构

    目前总共包含了 8 个 plugin hooks(如果左侧红色 Plugins),自动执行 / 缓存 / 防抖 / 节流 / 轮询 / loading 延迟 / 错误重试 / 屏幕重新请求:
    截屏2022-04-21 14.59.10.png

    插件初始化

    除了 plugin hooks 本身需要导出外,还需要导出 onInit 函数,提供给 useRequest 获取 initState 使用(如图红色 onInit),当然如果 plugin 本身不关注此钩子外,也无需导出

    插件生命周期

    plugin hooks 执行后可以返回本身插件所关注的,主异步流程执行的生命周期函数(如图右侧 run / runAsync 内红色标注的生命周期)

TS 类型定义

有了上面的大致分析,可以来查看相关的 TS 类型定义了,也有方便于接下来的源码解读:

useRequest 入参部分

总共三个参数,异步操作 + options + plugins,plugins 文档未公开使用,暂不讨论

  1. // hooks 的入参
  2. // TData 异步操作后的返回值,TParams 异步操作的入参
  3. function useRequest<TData, TParams extends any[]>(
  4. service: Service<TData, TParams>, // 异步操作
  5. options?: Options<TData, TParams>, // options
  6. plugins?: Plugin<TData, TParams>[], // 插件
  7. )
  • services:异步操作

    1. // 入参是 TParams,返回值是 Promise<TData>,是个异步函数
    2. type Service<TData, TParams extends any[]> = (...args: TParams) => Promise<TData>;
  • options:功能 options

    1. // 以下所有参数都是可选的,说明都是可有可无的,都是附加功能选项而已
    2. interface Options<TData, TParams extends any[]> {
    3. // 是否开启人工触发,区别就是第一次加载之后,会不会自动给你触发异步请求
    4. // 默认是会给你自动触发的,不需要的话需要自行 false 关闭
    5. manual?: boolean;
    6. // 这几个都是用户可以订阅的异步请求生命周期函数(对应上面图 run / runAsync 内蓝色部分标记)
    7. onBefore?: (params: TParams) => void;
    8. onSuccess?: (data: TData, params: TParams) => void;
    9. onError?: (e: Error, params: TParams) => void;
    10. // formatResult?: (res: any) => TData;
    11. onFinally?: (params: TParams, data?: TData, e?: Error) => void;
    12. // 默认请求参数
    13. defaultParams?: TParams;
    14. // refreshDeps
    15. // 主要对应到的是 useAutoRunPlugin 插件 hooks 内的功能
    16. refreshDeps?: DependencyList;
    17. refreshDepsAction?: () => void;
    18. ready?: boolean;
    19. // loading delay
    20. // 主要对应到的是 useLoadingDelayPlugin 插件 hooks 内的功能
    21. loadingDelay?: number;
    22. // polling
    23. // 主要对应到的是 usePollingPlugin 插件 hooks 内的功能
    24. pollingInterval?: number;
    25. pollingWhenHidden?: boolean;
    26. // refresh on window focus
    27. // 主要对应到的是 useRefreshOnWindowFocusPlugin 插件 hooks 内的功能
    28. refreshOnWindowFocus?: boolean;
    29. focusTimespan?: number;
    30. // debounce
    31. // 主要对应到的是 useDebouncePlugin 插件 hooks 内的功能
    32. debounceWait?: number;
    33. debounceLeading?: boolean;
    34. debounceTrailing?: boolean;
    35. debounceMaxWait?: number;
    36. // throttle
    37. // 主要对应到的是 useThrottlePlugin 插件 hooks 内的功能
    38. throttleWait?: number;
    39. throttleLeading?: boolean;
    40. throttleTrailing?: boolean;
    41. // cache
    42. //主要对应到的是 useCachePlugin 插件 hooks 内的功能
    43. cacheKey?: string;
    44. cacheTime?: number;
    45. staleTime?: number;
    46. setCache?: (data: CachedData<TData, TParams>) => void;
    47. getCache?: (params: TParams) => CachedData<TData, TParams> | undefined;
    48. // retry
    49. // 主要对应到的是 useRetryPlugin 插件 hooks 内的功能
    50. retryCount?: number;
    51. retryInterval?: number;
    52. }
  • plugins:plugins 文档未公开使用,但可以看到一个 plugin 定义的规范是怎样的 ```typescript // 这里是一个 Plugin 的定义 // 一个 Plugin = Plugin Hooks + onInit type Plugin = { // Plugin Hooks,props = 实例化的Fetch + 用户传入的 options // Plugin Hooks,return = 订阅异步过程的生命周期函数 list (fetchInstance: Fetch, options: Options): PluginReturn< TData, TParams

    ; // onInit,主要为获取 initState 使用 onInit?: (options: Options) => Partial>; };

// Plugin Hooks 的返回值定义 // 订阅异步过程的生命周期函数 list(对应上面图 run / runAsync 内红色部分标记) 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; }

  1. <a name="n5vh9"></a>
  2. #### useRequest 返回值部分
  3. ```typescript
  4. interface Result<TData, TParams extends any[]> {
  5. // 异步过程中的几个状态 & 数据 & 参数
  6. loading: boolean;
  7. data?: TData;
  8. error?: Error;
  9. params: TParams | [];
  10. // 提供给到外部,可以直接执行的方法
  11. // 暴露的方法都是从 Class Fetch 内直接获取的,从这里就可以看出 Fetch 的作用了
  12. cancel: Fetch<TData, TParams>['cancel'];
  13. refresh: Fetch<TData, TParams>['refresh'];
  14. refreshAsync: Fetch<TData, TParams>['refreshAsync'];
  15. run: Fetch<TData, TParams>['run'];
  16. runAsync: Fetch<TData, TParams>['runAsync'];
  17. mutate: Fetch<TData, TParams>['mutate'];
  18. }

源码分析

useRequest Main 流程

  1. function useRequestImplement<TData, TParams extends any[]>(
  2. service: Service<TData, TParams>,
  3. options: Options<TData, TParams> = {},
  4. plugins: Plugin<TData, TParams>[] = [],
  5. ) {
  6. // 1. 默认参数处理过程,如果 manual 没有传的话,默认处理成 false
  7. const { manual = false, ...rest } = options;
  8. const fetchOptions = {
  9. manual,
  10. ...rest,
  11. };
  12. // 2. 异步函数使用 useLatest 进行存储
  13. // https://ahooks.js.org/zh-CN/hooks/use-latest
  14. // 作用主要是保持最新值的 service,useLatest 内部使用 useRef 进行存储
  15. const serviceRef = useLatest(service);
  16. // 3. 生成强制组件渲染函数,提供给到 Fetch 使用的
  17. // https://ahooks.js.org/zh-CN/hooks/use-update
  18. const update = useUpdate();
  19. // 4. Fetch 实例化过程
  20. const fetchInstance = useCreation(() => {
  21. // 4.1 InitState,逐个 plugin 获取 state,进行合并
  22. const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
  23. // 4.2 Fetch 实例化
  24. return new Fetch<TData, TParams>(
  25. serviceRef,
  26. fetchOptions,
  27. update,
  28. Object.assign({}, ...initState),
  29. );
  30. }, []);
  31. fetchInstance.options = fetchOptions;
  32. // 5. 注册&执行插件过程,逐一调用并且存至 fetchInstance 的 pluginImpls 内
  33. // run all plugins hooks
  34. fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
  35. // 第一次加载后,针对 manual = false 的自动 run 处理
  36. useMount(() => {
  37. if (!manual) {
  38. // useCachePlugin can set fetchInstance.state.params from cache when init
  39. const params = fetchInstance.state.params || options.defaultParams || [];
  40. // @ts-ignore
  41. fetchInstance.run(...params);
  42. }
  43. });
  44. // 卸载的时候,调用 fetchInstance 的 cancel 函数进行销毁即可
  45. useUnmount(() => {
  46. fetchInstance.cancel();
  47. });
  48. // hooks 导出的,实则全是 fetchInstance 内的值 或者 方法,所以关键主要看 Fetch 的实现
  49. return {
  50. loading: fetchInstance.state.loading,
  51. data: fetchInstance.state.data,
  52. error: fetchInstance.state.error,
  53. params: fetchInstance.state.params || [],
  54. cancel: useMemoizedFn(fetchInstance.cancel.bind(fetchInstance)),
  55. refresh: useMemoizedFn(fetchInstance.refresh.bind(fetchInstance)),
  56. refreshAsync: useMemoizedFn(fetchInstance.refreshAsync.bind(fetchInstance)),
  57. run: useMemoizedFn(fetchInstance.run.bind(fetchInstance)),
  58. runAsync: useMemoizedFn(fetchInstance.runAsync.bind(fetchInstance)),
  59. mutate: useMemoizedFn(fetchInstance.mutate.bind(fetchInstance)),
  60. } as Result<TData, TParams>;
  61. }

Class Fetch

先从大致的结构开始分析,Fetch 主要存储了什么,还有提供了些什么内部的处理方法:

  1. export default class Fetch<TData, TParams extends any[]> {
  2. // 保存所有的 plugin hooks
  3. pluginImpls: PluginReturn<TData, TParams>[];
  4. // 计时器,执行了多少遍
  5. count: number = 0;
  6. // 异步数据管理
  7. // loading:是否正在执行
  8. // params:请求参数
  9. // data:返回值
  10. // error:错误时候的值
  11. state: FetchState<TData, TParams> = {
  12. loading: false,
  13. params: undefined,
  14. data: undefined,
  15. error: undefined,
  16. };
  17. constructor(
  18. // 用户传进来的异步操作,并且经过 useLatest hooks 导出的 ref 最新值
  19. public serviceRef: MutableRefObject<Service<TData, TParams>>,
  20. // 用户传进来的 options
  21. public options: Options<TData, TParams>,
  22. // 订阅操作,这里主要传了 useUpdate,强制组件刷新函数进来
  23. public subscribe: Subscribe,
  24. // 所有插件的 initState 合集(遍历各个插件的 onInit 方法后,得到的值,合并后得来的)
  25. public initState: Partial<FetchState<TData, TParams>> = {},
  26. ) {
  27. // 整一个大合并,到 state
  28. this.state = {
  29. ...this.state,
  30. // 如果不需要手工执行,那么实例化的时候就是 loading 中的
  31. // 如果需要手工执行,那么实例化的时候就不是 loading 中的
  32. loading: !options.manual,
  33. ...initState,
  34. };
  35. }
  36. // 合并 state 操作,再触发订阅函数即可
  37. setState(s: Partial<FetchState<TData, TParams>> = {}) {
  38. this.state = {
  39. ...this.state,
  40. ...s,
  41. };
  42. this.subscribe();
  43. }
  44. // 执行插件的生命周期函数,遍历执行,并且返回数据大集合
  45. runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
  46. // @ts-ignore
  47. const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);
  48. return Object.assign({}, ...r);
  49. }
  50. // ---------------------------------
  51. // 下面的在下面展开讲讲,先大概记下这几个方法是干什么的
  52. // 两个主要执行异步操作的方法
  53. async runAsync(...params: TParams): Promise<TData> { ... }
  54. run(...params: TParams) { ... }
  55. // 一个取消方法
  56. cancel() { ... }
  57. // 两个刷新的方法
  58. refresh() { ... }
  59. refreshAsync() { ... }
  60. // 一个立即修改 data 数据的方法
  61. mutate(data?: TData | ((oldData?: TData) => TData | undefined)) { ... }
  62. }

再展开讲讲:

  • run / runAsnyc(对应流程图 run / runAsync): ```typescript async runAsync(…params: TParams): Promise { // 1. 每执行一次都会 count + 1,并且记录下当前的执行 count 到 currentCount 内 // 这个点可以跳跃到下面有个判断 currentCount !== this.count 这里粗略看看,主要是判断是不是被 cancel 使用的 // 也可以看看 cancel 的操作先,cancel 内会对 this.count + 1 this.count += 1; const currentCount = this.count;

    // 2. 遍历执行插件的 onBefore 钩子函数 const { stopNow = false, returnNow = false, …state } = this.runPluginHandler(‘onBefore’, params);

    // 3. 如果插件操作后的结果,是 stopNow,则停止 // stop request if (stopNow) { return new Promise(() => {}); }

    // 4. 设置当前 state // 这里可以看到插件生命周期钩子 onBefore 也会影响 state this.setState({ loading: true, params, …state, });

    // 5. 如果插件操作后的结果,是 returnNow,则获取当前的 state 内的 data 进行 resolve 返回 // return now if (returnNow) { return Promise.resolve(state.data); }

    // 6. 执行用户传入的 onBefore 生命周期钩子 this.options.onBefore?.(params);

    // 7. 执行异步过程重头戏!整一个 try catch // 异步过程的成功与失败的主要执行过程其实是类似的 try { // —-执行部分——————————————————————- // 7.1 执行插件的 onRequest 钩子,有可能会在插件内替换掉这个 service // replace service let { servicePromise } = this.runPluginHandler(‘onRequest’, this.serviceRef.current, params); if (!servicePromise) {

    1. servicePromise = this.serviceRef.current(...params);

    } // 7.2 执行 service const res = await servicePromise; // —-执行部分——————————————————————-

  1. // 下面这段开始,其实跟 catch error 逻辑大部分类似的
  2. // ---执行成功部分---------------------------------------------
  3. // 判断是不是被 cancel 了
  4. if (currentCount !== this.count) {
  5. // prevent run.then when request is canceled
  6. return new Promise(() => {});
  7. }
  8. // 设置新的 state,error 要清空
  9. this.setState({
  10. data: res,
  11. error: undefined,
  12. loading: false,
  13. });
  14. // 先执行用户的 onSuccess 生命周期钩子函数
  15. this.options.onSuccess?.(res, params);
  16. // 再插件的 onSuccess 生命周期钩子函数
  17. this.runPluginHandler('onSuccess', res, params);
  18. // 先执行用户的 onFinally 生命周期钩子函数
  19. this.options.onFinally?.(params, res, undefined);
  20. // 再插件的 onFinally 生命周期钩子函数
  21. if (currentCount === this.count) {
  22. this.runPluginHandler('onFinally', params, res, undefined);
  23. }
  24. return res; // 返回成功值
  25. // ---执行成功部分---------------------------------------------

} catch (error) { // —-执行失败部分——————————————————————- if (currentCount !== this.count) { // prevent run.then when request is canceled return new Promise(() => {}); }

  1. // 设置新的 state,data 不会覆盖上次的
  2. this.setState({
  3. error,
  4. loading: false,
  5. });
  6. // 先执行用户的 onError 生命周期钩子函数
  7. this.options.onError?.(error, params);
  8. // 再插件的 onError 生命周期钩子函数
  9. this.runPluginHandler('onError', error, params);
  10. // 先执行用户的 onFinally 生命周期钩子函数
  11. this.options.onFinally?.(params, undefined, error);
  12. // 再插件的 onFinally 生命周期钩子函数
  13. if (currentCount === this.count) {
  14. this.runPluginHandler('onFinally', params, undefined, error);
  15. }
  16. throw error; // 抛出异常
  17. // ---执行失败部分---------------------------------------------

} }

// 实际只是调用 runAsync // 作用就是在 useRequest 内部进行 catch 住 error,不对外抛 // 如果使用 runAsync 的话,就需要用户自己 catch 住错误 run(…params: TParams) { this.runAsync(…params).catch((error) => { if (!this.options.onError) { console.error(error); } }); }

  1. - **cancel:(对应流程图 cancel)**
  2. 1. count + 1 处理
  3. 1. 事实上判断当前请求是不是被取消,是通过这个 count 去判断的,对应到 runAsync 内有个判断(**currentCount !== this.count** 的话,就直接返回空的 Promise
  4. 1. cancel 过程是同步的,异步过程是异步的,所有就能判断到不一致的情况
  5. 1. **这里要注意的是,异步过程终究还是执行了,不是真的对这个异步过程取消执行,只是这个过程执行了,但不需要返回而已**
  6. 2. 设置 state loading false
  7. 2. 执行插件的 onCanel 钩子函数
  8. ```typescript
  9. cancel() {
  10. this.count += 1;
  11. this.setState({
  12. loading: false,
  13. });
  14. this.runPluginHandler('onCancel');
  15. }
  • refresh / refreshAsync(对应流程图 refresh / refreshAsnyc):只是对 run 进行包装一层,用的是上次的存储的 params 进行重刷 ```typescript refresh() { // @ts-ignore this.run(…(this.state.params || [])); }

refreshAsync() { // @ts-ignore return this.runAsync(…(this.state.params || [])); }

  1. - **mutate: **立即执行更新内部 data 数据,并且触发 plugins onMutate 生命周期钩子函数
  2. ```typescript
  3. mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
  4. let targetData: TData | undefined;
  5. if (typeof data === 'function') {
  6. // @ts-ignore
  7. targetData = data(this.state.data);
  8. } else {
  9. targetData = data;
  10. }
  11. this.runPluginHandler('onMutate', targetData);
  12. this.setState({
  13. data: targetData,
  14. });
  15. }

Plugin Hooks

useAutoRunPlugin

依赖刷新 & ready 功能 主要处理 options 内 refreshDeps / refreshDepsAction/ ready 参数逻辑

  • 内部 hasAutoRun 保存了当前异步操作是否正在执行,每次重新执行该 hooks 时,hasAutoRun 都会被默认初始化为 false,如果 ready 与 refreshDeps 都同时改变了,那么会首先执行 ready 的副作用函数,ready 内操作使用 defaultParams 执行 fetchInstance.run,并同时设置 hasAutoRun 为 true。那么 refreshDeps 触发的 effect 操作就不会被执行(因为里面首先判断 hasAutoRun 为 true 时,不执行)
  • useUpdateEffect 会忽略首次执行,只在依赖更新时执行
  • ready 参数 = false,即当前异步操作没准备好,那么需要中断流程,中断流程是通过暴露 onBefore 钩子,返回 stopNow = true,进行异步流程中断的
  • refreshDeps 的副作用函数内,是否执行 refreshDepsAction / refresh,仅仅只是判断了 manual,没有通过 ready 去判断,所以但凡 refreshDeps 变更了,都会执行到 refreshDepsAction / refresh。因为 ready = false 中断,是通过 onBefore 返回的 stopNow 标志去中断的。
  • 另外,在之前针对 Fetch 实例化时,判断 loading 默认 true / false,只针对 manual 去判断,那么需要加上 ready 参数来联合判断,因此需要导出 onInit 钩子函数 ```typescript // support refreshDeps & ready const useAutoRunPlugin: Plugin = ( fetchInstance, { manual, ready = true, defaultParams = [], refreshDeps = [], refreshDepsAction }, ) => { const hasAutoRun = useRef(false); hasAutoRun.current = false; // 每次组件渲染都会初始化为 false

    useUpdateEffect(() => { // 如果 ready 并且不需要人工触发的话,则需要自动执行 run if (!manual && ready) {

    1. hasAutoRun.current = true;
    2. // 开启 ready 的话,使用 defaultParams 执行 run
    3. fetchInstance.run(...defaultParams);

    } }, [ready]);

    useUpdateEffect(() => { // 说明正在执行上面的 run if (hasAutoRun.current) {

    1. return;

    }

    // 这里只判断了 manual 的情况,没有把 ready 参数带进来,本质有没有应该无所谓? // 如果有 ready 判断,refresh 操作执行可以忽略 // 目前来看的话,其实每次 refreshDeps 变化,都会执行到 refresh if (!manual) {

    1. hasAutoRun.current = true;
    2. // 如果有传递 refreshDepsAction 操作函数,则不会执行 fetch 的刷新操作
    3. if (refreshDepsAction) {
    4. refreshDepsAction();
    5. } else {
    6. fetchInstance.refresh();
    7. }

    } }, […refreshDeps]);

    return { // 订阅 onBefore 钩子,返回 stopNow 标志 onBefore: () => {

    1. // 如果 ready 为 false,则设置 stopNow 标志进行流程中断
    2. if (!ready) {
    3. return {
    4. stopNow: true,
    5. };
    6. }

    }, }; };

// 在 Fetch 内原来的 loading 判断逻辑,是单独通过 manual 来判断的 // 但是这里加上 ready 之后,其实需要多一个判断逻辑 // 非手工触发 并且 准备好之后,loading 才是 true // 否则,loading 都是 false 的,一开始都是不自动执行的 useAutoRunPlugin.onInit = ({ ready = true, manual }) => { return { loading: !manual && ready, }; };

  1. <a name="jlMyH"></a>
  2. ####
  3. <a name="rKXSh"></a>
  4. #### useDebouncePlugin / useThrottlePlugin
  5. > 防抖 / 节流,两个处理操作差不多,一起讲
  6. > 主要处理 options 内 debounceWait / debounceLeading / debounceTrailing / debounceMaxWait / throttleWait / throttleLeading / throttleTrailing
  7. - debounce 依赖 lodash/debounce 功能,throttle 依赖 lodash / throttle 功能
  8. - [https://lodash.com/docs/4.17.15#debounce](https://lodash.com/docs/4.17.15#debounce)
  9. - [https://lodash.com/docs/4.17.15#throttle](https://lodash.com/docs/4.17.15#throttle)
  10. - 对 options 进行重新格式化,转成 lodash 内置函数参数
  11. - 防抖 / 节流,针对 fetchInstance.runAsync 函数进行重写,加一层 debounce / throttle 函数进行执行,当组件销毁时,取消 debounce / throttle,并且对 fetchInstance.runAsync 函数进行还原
  12. - 如果开启防抖 / 节流功能,需要订阅 onCanel 钩子函数,进行 取消 debounce / throttle
  13. ```typescript
  14. const useDebouncePlugin: Plugin<any, any[]> = (
  15. fetchInstance,
  16. { debounceWait, debounceLeading, debounceTrailing, debounceMaxWait },
  17. ) => {
  18. const debouncedRef = useRef<DebouncedFunc<any>>();
  19. // 1. 参数格式化
  20. const options = useMemo(() => {
  21. const ret: DebounceSettings = {};
  22. if (debounceLeading !== undefined) {
  23. ret.leading = debounceLeading;
  24. }
  25. if (debounceTrailing !== undefined) {
  26. ret.trailing = debounceTrailing;
  27. }
  28. if (debounceMaxWait !== undefined) {
  29. ret.maxWait = debounceMaxWait;
  30. }
  31. return ret;
  32. }, [debounceLeading, debounceTrailing, debounceMaxWait]);
  33. // 2. 如果 debounceWait, options 参数变化,就触发副作用函数
  34. useEffect(() => {
  35. if (debounceWait) {
  36. // 保存原来的 fetchInstance.runAsync
  37. const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
  38. debouncedRef.current = debounce(
  39. (callback) => {
  40. callback();
  41. },
  42. debounceWait,
  43. options,
  44. );
  45. // 改写 fetchInstance.runAsync,加一层防抖或者节流,在回调内去执行真正的 runAsync
  46. // debounce runAsync should be promise
  47. // https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
  48. fetchInstance.runAsync = (...args) => {
  49. return new Promise((resolve, reject) => {
  50. debouncedRef.current?.(() => {
  51. _originRunAsync(...args)
  52. .then(resolve)
  53. .catch(reject);
  54. });
  55. });
  56. };
  57. // 销毁时,取消节流 / 防抖函数并且还原回原来的 fetchInstance.runAsync 函数
  58. return () => {
  59. debouncedRef.current?.cancel();
  60. fetchInstance.runAsync = _originRunAsync;
  61. };
  62. }
  63. }, [debounceWait, options]);
  64. // 如果没有开启防抖 / 节流,则不需要返回生命周期钩子函数
  65. if (!debounceWait) {
  66. return {};
  67. }
  68. // 如果开启防抖 / 节流,需要订阅 onCanel 的钩子函数
  69. // 主要为了取消防抖 / 节流
  70. return {
  71. onCancel: () => {
  72. debouncedRef.current?.cancel();
  73. },
  74. };
  75. };

useThrottlePlugin 类似,不另外赘述

useLoadingDelayPlugin

可以延迟 loading 变成 true 的时间,有效防止闪烁。 主要处理 options 内 loadingDelay

  • 订阅 onBefore 操作,在这个期间,注册一个定时器,定时器就是延迟 xx 秒,再设置 loading 为 true。同时改写当前的 loading 状态为 false
  • onFinally 和 onCanel,都需要取消这个定时器,有几种情况,如果执行时异步时间 < loading延迟时间,因为 onFinally 会取消定时器,故 loading 效果不会显示出来,也不会造成闪退的问题,其他情况也可以自行脑补下

    1. const useLoadingDelayPlugin: Plugin<any, any[]> = (fetchInstance, { loadingDelay }) => {
    2. const timerRef = useRef<Timeout>();
    3. if (!loadingDelay) {
    4. return {};
    5. }
    6. const cancelTimeout = () => {
    7. if (timerRef.current) {
    8. clearTimeout(timerRef.current);
    9. }
    10. };
    11. return {
    12. onBefore: () => {
    13. cancelTimeout();
    14. // 延迟设置 loading = true
    15. timerRef.current = setTimeout(() => {
    16. fetchInstance.setState({
    17. loading: true,
    18. });
    19. }, loadingDelay);
    20. // 先改写成 false
    21. return {
    22. loading: false,
    23. };
    24. },
    25. onFinally: () => {
    26. cancelTimeout(); // 取消定时器
    27. },
    28. onCancel: () => {
    29. cancelTimeout(); // 取消定时器
    30. },
    31. };
    32. };

useRetryPlugin

通过设置 options.retryCount,指定错误重试次数,则 useRequest 在失败后会进行重试。 主要处理 options 内 retryCount / retryInterval

整体重试流程可以概括为以下的流程图: ✅ useRequest - 图4

  • count 记录当前重试次数,triggerByRetry 记录当前的请求,是不是重试触发的,time 存储当前的定时器
  • onError 内先对当前的重试次数 + 1,如果说在重试次数之内的话,则设置 setTimeout 进行重试,否则对 count 清零
  • 理解难点在 triggerByRetry,需要区分当前请求是不是重试触发的,主要是为了解决未到重试时间时,人为触发了异步过程。如果说人为触发了,需要对之前未到的重试 setTimeout 清除,并且重置 count,具体可以看以下代码注释
  • onSuccess 与 onCancel,都是类似的操作,对 count 清零,并且清除已存在的定时器(onSuccess 代码里面没有 clearTimeout,应该有问题)

    1. const useRetryPlugin: Plugin<any, any[]> = (fetchInstance, { retryInterval, retryCount }) => {
    2. const timerRef = useRef<Timeout>();
    3. const countRef = useRef(0);
    4. const triggerByRetry = useRef(false);
    5. // 如果没有开启重试功能,不需要返回钩子函数
    6. if (!retryCount) {
    7. return {};
    8. }
    9. return {
    10. onBefore: () => {
    11. // 说明这是人为导致的异步触发,需要重置 count
    12. // 因为重试执行的异步过程 triggerByRetry 为 true
    13. // 可以看到 setTimeout 里面 refresh 之前,设置了 triggerByRetry = true
    14. if (!triggerByRetry.current) {
    15. countRef.current = 0;
    16. }
    17. // 每次请求前,都会重置 triggerByRetry 标志为 false
    18. // 并且清除已有的定时器
    19. // 因为有可能在定时器还没到时间之前,人为进行触发了异步,那么之前在等待重试的异步过程,需要清掉
    20. triggerByRetry.current = false;
    21. if (timerRef.current) {
    22. clearTimeout(timerRef.current);
    23. }
    24. },
    25. onSuccess: () => {
    26. // onSuccess 这里只是 count 清零,没有清除已有的定时器,是否有问题?
    27. // 正在 setTimeout 等待重试的过程中,用户手动点击重刷异步操作,并且成功了,这个时候应该是要清除的?
    28. countRef.current = 0;
    29. },
    30. onError: () => {
    31. countRef.current += 1;
    32. if (retryCount === -1 || countRef.current <= retryCount) {
    33. // Exponential backoff
    34. const timeout = retryInterval ?? Math.min(1000 * 2 ** countRef.current, 30000);
    35. timerRef.current = setTimeout(() => {
    36. triggerByRetry.current = true; // 调用在重试之前,会将 triggerByRetry 设置为 true
    37. fetchInstance.refresh(); // 调用刷新
    38. }, timeout);
    39. } else {
    40. countRef.current = 0;
    41. }
    42. },
    43. onCancel: () => {
    44. countRef.current = 0;
    45. if (timerRef.current) {
    46. clearTimeout(timerRef.current);
    47. }
    48. },
    49. };
    50. };

usePollingPlugin

通过设置 options.pollingInterval,进入轮询模式,useRequest 会定时触发 service 执行。 主要处理 options 内 pollingInterval

  • onFinally 内,设置 setTimeout 定时器,进行 refresh
  • onFinally 内,如果设置了 pollingWhenHidden = false(页面隐藏时,停止轮询),并且当前标签页不是激活状态下,订阅【标签页激活事件】,停止轮询直接 return,那么当页面再次打开时,则会执行回调进行异步 refresh。执行 refresh 后,当再次进入 onFinally 钩子时,因为不命中标签页隐藏条件, 则又开启轮询
  • onBefore & onCancel,clearTimeout
  • 监听 pollingInterval 值,从 大于零 —更新为—> 零,同样需要 clear 操作 ```typescript const usePollingPlugin: Plugin = ( fetchInstance, { pollingInterval, pollingWhenHidden = true }, ) => { const timerRef = useRef(); const unsubscribeRef = useRef<() => void>();

    // 停止定时器,并且退订标签页激活事件 const stopPolling = () => { if (timerRef.current) {

    1. clearTimeout(timerRef.current);

    } unsubscribeRef.current?.(); };

    // 若关闭轮询,则销毁定时器 useUpdateEffect(() => { if (!pollingInterval) {

    1. stopPolling();

    } }, [pollingInterval]);

// 若没有开启轮询功能,不需要返回钩子事件 if (!pollingInterval) { return {}; }

return { onBefore: () => { stopPolling(); }, onFinally: () => { // 如果设置 pollingWhenHidden = false 并且 当前标签页隐藏状态 // 仅仅只是订阅 re visible 事件,停止轮询 return // 当再次激活标签页时,触发 refresh 重刷接口 if (!pollingWhenHidden && !isDocumentVisible()) { unsubscribeRef.current = subscribeReVisible(() => { fetchInstance.refresh(); }); return; }

  1. // 设置轮询
  2. timerRef.current = setTimeout(() => {
  3. fetchInstance.refresh();
  4. }, pollingInterval);
  5. },
  6. onCancel: () => {
  7. stopPolling();
  8. },

}; };

  1. 对于标签页激活,实现了一个简单的发布-订阅:
  2. ```typescript
  3. const listeners: any[] = []; // 全局保存了回调操作 list
  4. // 事件订阅
  5. function subscribe(listener: () => void) {
  6. listeners.push(listener); // 放于 list 内
  7. // 返回退订函数,退订函数内知识讲该回调剔除出 list 之外
  8. return function unsubscribe() {
  9. const index = listeners.indexOf(listener);
  10. listeners.splice(index, 1);
  11. };
  12. }
  13. if (canUseDom()) {
  14. // visibilitychange 事件,当前如果隐藏,则不操作
  15. // 当前如果是激活,则遍历 listeners,逐个回调依次执行
  16. const revalidate = () => {
  17. if (!isDocumentVisible()) return;
  18. for (let i = 0; i < listeners.length; i++) {
  19. const listener = listeners[i];
  20. listener();
  21. }
  22. };
  23. // 监听 visibilitychange 事件
  24. window.addEventListener('visibilitychange', revalidate, false);
  25. }

useRefreshOnWindowFocusPlugin

通过设置 options.refreshOnWindowFocus,在浏览器窗口 refocus 和 revisible 时,会重新发起请求。 主要处理 options 内 refreshOnWindowFocus

  • 当 refreshOnWindowFocus = true 时,订阅 focus 事件,当 focus 时,重试执行异步的 refresh 操作,这里的 limit 函数限制了执行间隔,可以看到具体的实现
  • 在组件销毁 / refreshOnWindowFocus 变为 false 时,注销订阅事件
  • 具体 subscribeFocus 实现,与上面 subscribeReVisible 实现类似,全局管理了一个 listeners 队列,回调后遍历执行

    1. const useRefreshOnWindowFocusPlugin: Plugin<any, any[]> = (
    2. fetchInstance,
    3. { refreshOnWindowFocus, focusTimespan = 5000 },
    4. ) => {
    5. const unsubscribeRef = useRef<() => void>();
    6. // 注销
    7. const stopSubscribe = () => {
    8. unsubscribeRef.current?.();
    9. };
    10. useEffect(() => {
    11. // refreshOnWindowFocus = true 时候订阅执行
    12. if (refreshOnWindowFocus) {
    13. const limitRefresh = limit(fetchInstance.refresh.bind(fetchInstance), focusTimespan);
    14. unsubscribeRef.current = subscribeFocus(() => {
    15. limitRefresh();
    16. });
    17. }
    18. // 注销
    19. return () => {
    20. stopSubscribe();
    21. };
    22. }, [refreshOnWindowFocus, focusTimespan]);
    23. // 注销
    24. useUnmount(() => {
    25. stopSubscribe();
    26. });
    27. // 不需要监听任何生命周期,因为这是个一次性的操作,与任何请求阶段无关
    28. return {};
    29. };

    用 pending 标志控制是否执行,pending 为 false 的时候执行 fn,当 timespan 后,再设置会标志为 pending 为 true,因此在 timespan 内 pending = true,都会被直接 return 掉。以此来控制 fn 执行的时间间隔。 ```typescript export default function limit(fn: any, timespan: number) { let pending = false; return (…args: any[]) => { if (pending) return; pending = true; fn(…args); setTimeout(() => {

    1. pending = false;

    }, timespan); }; }

  1. <a name="YO3EI"></a>
  2. #### useCachePlugin
  3. > SWR,数据缓存能力
  4. > 主要处理 options 内 cacheKey / cacheTime / staleTime / setCache / getCache
  5. **数据新鲜**<br />先查看 utils/cache 代码,实现了一个简单的内存 cache 操作:
  6. - 全局保存了 cache Map,以 cacheKey 作为 key,对应 value 存储了 data - 异步返回结果,params - 请求参数,time - 清除缓存定时器(默认 cacheTime = 5min,plugin hooks 默认参数设置)![截屏2022-04-25 16.24.11.png](https://cdn.nlark.com/yuque/0/2022/png/22895623/1650875070532-3dec5097-7139-4889-9e0f-c5d17b4448d4.png#clientId=u4933b4de-7830-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=148&id=cDQgW&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2022-04-25%2016.24.11.png&originHeight=296&originWidth=908&originalType=binary&ratio=1&rotation=0&showTitle=false&size=45874&status=done&style=none&taskId=u99798829-afdd-4f71-a34e-5694fe621cf&title=&width=454)
  7. - setCache,首先对已有 key 的 cache 进行清除(主要是清除计时器),再针对这个 key 进行重新注册
  8. - 同时暴露 getCache 与 clearCache 操作,对应实现也比较简单
  9. ```typescript
  10. export interface CachedData<TData = any, TParams = any> {
  11. data: TData; // 异步执行结果
  12. params: TParams; // 异步请求参数
  13. time: number; // 缓存那一刻的时间
  14. }
  15. interface RecordData extends CachedData {
  16. timer: Timer | undefined; // 清除缓存定时器
  17. }
  18. // map 存储数据,全局一份
  19. const cache = new Map<CachedKey, RecordData>();
  20. // 设置key 对应的 cache
  21. const setCache = (key: CachedKey, cacheTime: number, cachedData: CachedData) => {
  22. // 先清除
  23. const currentCache = cache.get(key);
  24. if (currentCache?.timer) {
  25. clearTimeout(currentCache.timer);
  26. }
  27. // 再重新登记
  28. let timer: Timer | undefined = undefined;
  29. // 如果是 -1 的话,timer 为空,永不清除
  30. if (cacheTime > -1) {
  31. // if cache out, clear it
  32. timer = setTimeout(() => {
  33. cache.delete(key);
  34. }, cacheTime);
  35. }
  36. cache.set(key, {
  37. ...cachedData,
  38. timer,
  39. });
  40. };
  41. // 获取对应 cache
  42. const getCache = (key: CachedKey) => {
  43. return cache.get(key);
  44. };
  45. // 清除某些 cache,可以一次清除多个或者一个,不传就是全清除
  46. const clearCache = (key?: string | string[]) => {
  47. if (key) {
  48. const cacheKeys = Array.isArray(key) ? key : [key];
  49. cacheKeys.forEach((cacheKey) => cache.delete(cacheKey));
  50. } else {
  51. cache.clear();
  52. }
  53. };

对于基础的异步数据缓存能力,结合代码,大致可以分为以下几种情况来描述:

流程描述 对应代码实现(主要关注红框部分实现即可) 实际业务使用场景
没有命中数据缓存的情况下:需要等到异步数据返回,才能展示数据
- loading:false -> true
- data:无 -> 新
截屏2022-04-25 18.20.05.png onBefore:
不拦截请求,返回空数据即可
截屏2022-04-25 17.48.55.png

onSuccess:
如果设置了 cacheKey,则设置
截屏2022-04-25 17.50.47.png
loading && !data 联合判断下:
- loading 状态会显示出来
- 异步结束时,展示数据
命中数据缓存,在 cacheTime 时间内,但不在 slateTime 时间内:先从 cache 获取展示旧 data,不阻塞异步操作,等异步数据回来后,再直接更新展示新 data
- loading:false -> true
- data:旧 -> 新
截屏2022-04-25 18.23.22.png onBefore
在请求前,先从 cache 获取数据,先写到 data 里面,loading 和 params 还是正常请求的状态值
截屏2022-04-25 18.30.42.png

onSuccess:
如果设置了 cacheKey,则设置
截屏2022-04-25 17.50.47.png

hooks 初始化时:
命中缓存的情况下(缓存时间不在 slateTime 内),会从 cache 内获取数据先直接写入 params 和 data
截屏2022-04-25 18.33.22.png
loading && !data 联合判断下:
- loading 状态不会显示,会默认展示到上一次的数据
- 异步结束后,直接重新渲染新的数据
命中数据缓存,并且在 slateTime 时间内:不从异步操作获取数据,直接从 cache 获取
- loading:始终 false
- data:无 / 旧 -> 新
截屏2022-04-25 18.20.10.png onBefore:
slateTime 等于 -1 说明永远新鲜,或者 cache 时间间隔在 slateTime 内,则直接从 cache get 数据,返回 returnNow = true 标志停止异步过程
截屏2022-04-25 18.02.47.png

onSuccess
onBefore 直接 return 掉了,不会走到这个钩子的

hooks 初始化时:
在 slateTime 时间内,不需要展示 loading 状态(大框框的逻辑命中缓存时间内都会走到,小框框是只有 slateTime 时间内才会走,区别只是需不需要设置 loading 状态而已)
image.png
loading && !data 联合判断下:
- loading 状态不会显示,会默认展示到上一次的数据
- 上一次的数据,就是最新的数据

总结来说,具体缓存时间可以以下概括:
截屏2022-04-25 19.15.31.png

  • 最开始没有缓存的时候,会触发异步操作,成功后,写 cache
  • 深红色新鲜时间内,不会触发异步过程,直接读取 cache 数据
  • 超过新鲜时间后,仍在缓存时间内,在展示旧数据的同时,会异步拉取数据,当数据回来后,再展示到新的数据,同时重新设置上新的 cache
  • 若人为触发 clearCache,再重新触发异步,则再次循环以上过程

数据共享
若分别在两个 component 下,看下是怎么实现数据共享的,主要有两点:

  1. 对 service promise 的共享,同一份数据,接口只会发出一次
  2. 对数据的共享,A 组件已经获取过一次最新的数据,那么 B 如果也读到这份数据的话,不需要重复拉取

第一点,是怎么实现对异步操作 promise 缓存的,大致流程可以以下图概括,若 ComponentA 已经触发了异步请求,会对这个 service promise 缓存在全局内存中,当 ComponentB 组件紧接着也被触发之后,会直接从内存读取到这个 service promise:
截屏2022-04-25 19.26.39.png
utils / cachePromise 实现了对一个 Promise 的缓存:

  1. type CachedKey = string | number;
  2. // 缓存 Map,key 是 cacheKey
  3. const cachePromise = new Map<CachedKey, Promise<any>>();
  4. // 读取 promise cache
  5. const getCachePromise = (cacheKey: CachedKey) => {
  6. return cachePromise.get(cacheKey);
  7. };
  8. // 写入 promise cache
  9. const setCachePromise = (cacheKey: CachedKey, promise: Promise<any>) => {
  10. // Should cache the same promise, cannot be promise.finally
  11. // Because the promise.finally will change the reference of the promise
  12. cachePromise.set(cacheKey, promise);
  13. // no use promise.finally for compatibility
  14. // 在 promise 结束的时候,then 和 cache 再删除 cache 即可
  15. promise
  16. .then((res) => {
  17. cachePromise.delete(cacheKey);
  18. return res;
  19. })
  20. .catch(() => {
  21. cachePromise.delete(cacheKey);
  22. });
  23. };

useCachePlugin 内的 onRequest 操作,以及 Fetch 内的 runAsync 过程:

  1. // 关注 useCachePlugin 内的 onRequest:
  2. const useCachePlugin: Plugin<any, any[]> = (
  3. fetchInstance,
  4. {
  5. cacheKey,
  6. cacheTime = 5 * 60 * 1000,
  7. staleTime = 0,
  8. setCache: customSetCache,
  9. getCache: customGetCache,
  10. },
  11. ) => {
  12. const currentPromiseRef = useRef<Promise<any>>();
  13. // ...
  14. return {
  15. onBefore: (params) => {
  16. // ...
  17. },
  18. onRequest: (service, args) => {
  19. // 1: 先从 cache 读 promise
  20. let servicePromise = cachePromise.getCachePromise(cacheKey);
  21. // 2.1: 如果有,则直接返回这个 promise
  22. if (servicePromise && servicePromise !== currentPromiseRef.current) {
  23. return { servicePromise };
  24. }
  25. // 2.2: 如果没有,则构造一个缓存,然后返回这个 promise
  26. servicePromise = service(...args);
  27. currentPromiseRef.current = servicePromise;
  28. cachePromise.setCachePromise(cacheKey, servicePromise);
  29. return { servicePromise };
  30. },
  31. onSuccess: (data, params) => {
  32. // ...
  33. },
  34. onMutate: (data) => {
  35. // ...
  36. },
  37. };
  38. };
  39. // Fetch 的 runAsync 操作:
  40. async runAsync() {
  41. try {
  42. // 1. 从 onRequest 钩子拿到 servicePromise
  43. // 有可能是缓存的,也有可能不是,不是的话会顺便缓存一个
  44. let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);
  45. // 2. 兜底空的情况
  46. if (!servicePromise) {
  47. servicePromise = this.serviceRef.current(...params);
  48. }
  49. // 3. 执行这个 promise 获取数据
  50. const res = await servicePromise;
  51. // ...
  52. }
  53. }

针对第二点,数据的共享,大致流程图如下,假如 ComponentA 是被触发异步操作的,并且 ComponentB 渲染的数据与 ComponentA 是同一份(ComponentB 没有被触发异步操作):
截屏2022-04-25 19.49.36.png
可以先看下 utils/cacheSubscribe,实现了针对 cache key set 操作的发布-订阅:

  1. type Listener = (data: any) => void;
  2. // 事件队列
  3. const listeners: Record<string, Listener[]> = {};
  4. // 触发过程,就是遍历 listeners 逐个传参数执行
  5. const trigger = (key: string, data: any) => {
  6. if (listeners[key]) {
  7. listeners[key].forEach((item) => item(data));
  8. }
  9. };
  10. const subscribe = (key: string, listener: Listener) => {
  11. if (!listeners[key]) {
  12. listeners[key] = [];
  13. }
  14. listeners[key].push(listener); // 订阅 push 函数到队列
  15. // 退订操作
  16. return function unsubscribe() {
  17. const index = listeners[key].indexOf(listener);
  18. listeners[key].splice(index, 1);
  19. };
  20. };
  21. export { trigger, subscribe };

结合 cacheSubscribe,在 useCachePlugin 实现相关逻辑:

  • 触发的时机,在 setCache 的时候
  • onSuccess 和 onMutate 的过程是类似的,过程都是先取消订阅,设置 cache,再重新订阅。这个过程有点迷,单看一个 component 可能分析不出来,其实完整的描述过程是(场景假设是只触发了 A 的异步,B 是被动更新),先取消订阅 A 的 trigger 事件(这样之后 set cache,就不会触发 A 本身 ),set cache(这时候会触发 B 的 trigger),再重新订阅回 A 的 trigger 事件(这样如果之后在 B 那边触发了,A 也是走这么个流程)
  • trigger 的回调都是 fetchInstance.setState({ data }),回忆一下 Fetch 内封装的逻辑,setState 会触发 update,update 强制更新 render

    1. const useCachePlugin: Plugin<any, any[]> = (
    2. fetchInstance,
    3. {
    4. cacheKey,
    5. cacheTime = 5 * 60 * 1000,
    6. staleTime = 0,
    7. setCache: customSetCache,
    8. getCache: customGetCache,
    9. },
    10. ) => {
    11. const unSubscribeRef = useRef<() => void>();
    12. // setCache 操作:会 trigger 队列里所有的事件
    13. const _setCache = (key: string, cachedData: CachedData) => {
    14. // ...
    15. cacheSubscribe.trigger(key, cachedData.data);
    16. };
    17. // ...
    18. useCreation(() => {
    19. // ...
    20. // 首次 hooks 注册订阅 cache set 事件
    21. // subscribe same cachekey update, trigger update
    22. unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (data) => {
    23. fetchInstance.setState({ data });
    24. });
    25. }, []);
    26. // 组件注销,退订 cache set
    27. useUnmount(() => {
    28. unSubscribeRef.current?.();
    29. });
    30. // ...
    31. return {
    32. onBefore: (params) => {
    33. // ...
    34. },
    35. onRequest: (service, args) => {
    36. // ...
    37. },
    38. onSuccess: (data, params) => {
    39. if (cacheKey) {
    40. // cancel subscribe, avoid trgger self
    41. unSubscribeRef.current?.(); // 1. 先注销自己
    42. // 2. 触发 set cache,set cache trigger 事件队列
    43. _setCache(cacheKey, {
    44. data,
    45. params,
    46. time: new Date().getTime(),
    47. });
    48. // resubscribe
    49. // 3. 重新订阅
    50. unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (d) => {
    51. fetchInstance.setState({ data: d });
    52. });
    53. }
    54. },
    55. // 类似 onSuccess 的步骤,不赘述
    56. onMutate: (data) => {
    57. if (cacheKey) {
    58. // cancel subscribe, avoid trgger self
    59. unSubscribeRef.current?.();
    60. _setCache(cacheKey, {
    61. data,
    62. params: fetchInstance.state.params,
    63. time: new Date().getTime(),
    64. });
    65. // resubscribe
    66. unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (d) => {
    67. fetchInstance.setState({ data: d });
    68. });
    69. }
    70. },
    71. };
    72. };

    综上所述,结合三个 utils 原材料,可以整体回顾下 useCachePlugin 的实现了,这里就不再重复总结了

    细节分析

    事实上 useRequest 内运用了很多基础 ahooks,比如 useUpdateEffect,useCreation 等,在看 useRequest 前,最后对其他基础的 ahooks 先有个大致印象会比较好,也更有助于阅读理解:

    Fetch 在 hooks 内是如何避免重复实例化的?

    使用的是 useCreation:https://ahooks.js.org/zh-CN/hooks/use-creation,具体实现如下:

  • 本质还是使用 useRef,将 factory 函数执行的结果保存在 current.obj 内

  • 使用 initialized 标记是不是已经初始化过 ```typescript import type { DependencyList } from ‘react’; import { useRef } from ‘react’; import depsAreSame from ‘../utils/depsAreSame’;

export default function useCreation(factory: () => T, deps: DependencyList) { const { current } = useRef({ deps, obj: undefined as undefined | T, initialized: false, });

// 只有 initialized 是 false // 或者 依赖列表发生变化的时候 // 才需要重新执行 factory(), 获取新的实例 if (current.initialized === false || !depsAreSame(current.deps, deps)) { current.deps = deps; current.obj = factory(); current.initialized = true; }

// 返回这个实例 return current.obj as T; }

  1. <a name="a0Si1"></a>
  2. ### Fetch 是 Class,数据也是存储在 Class 内的,如何通知外层 useRequest hooks 需要做组件更新?
  3. 使用的是 useUpdate:[https://ahooks.js.org/zh-CN/hooks/use-update](https://ahooks.js.org/zh-CN/hooks/use-update),具体实现如下:其实是设置了个空的 state,每次都强行设置 state,就能触发更新,然后把这个 hooks 返回的函数,传至 Fetch 内进行使用
  4. ```typescript
  5. const useUpdate = () => {
  6. const [, setState] = useState({});
  7. return useCallback(() => setState({}), []);
  8. };

Cancel 没有真正意义上的取消,只是不返回

  1. // Fetch cancel 对 count 进行 + 1 处理
  2. cancel() {
  3. this.count += 1;
  4. this.setState({
  5. loading: false,
  6. });
  7. this.runPluginHandler('onCancel');
  8. }
  9. // Fetch runAsync 执行后会判断 currentCount !== this.count
  10. async runAsync() {
  11. // ...
  12. try {
  13. // replace service
  14. let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);
  15. if (!servicePromise) {
  16. servicePromise = this.serviceRef.current(...params);
  17. }
  18. const res = await servicePromise;
  19. // 主要是这里
  20. if (currentCount !== this.count) {
  21. // prevent run.then when request is canceled
  22. return new Promise(() => {});
  23. }
  24. // ...
  25. }
  26. // ...
  27. }

每个 plugin hooks 其实都只是处理 options 内的某些参数而已

  1. // useRequest 内注册插件时
  2. // 会把当前的实例化Fetch 与 全量 options 往下传递
  3. fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
  4. // useAutoRunPlugin
  5. // 只接收了 manual / ready / defaultParams / refreshDeps / refreshDepsAction
  6. const useAutoRunPlugin: Plugin<any, any[]> = (
  7. fetchInstance,
  8. { manual, ready = true, defaultParams = [], refreshDeps = [], refreshDepsAction },
  9. ) => { ... }
  10. // useCachePlugin
  11. // 只接收了 cacheKey / cacheTime / staleTime / setCache / getCache
  12. const useCachePlugin: Plugin<any, any[]> = (
  13. fetchInstance,
  14. {
  15. cacheKey,
  16. cacheTime = 5 * 60 * 1000,
  17. staleTime = 0,
  18. setCache: customSetCache,
  19. getCache: customGetCache,
  20. },
  21. ) => { ... }
  22. // 等等以此类推

疑问点保留

  1. plugins 参数是开放出去的(系统默认带上公共插件),意味着用户可以自定义插件?
  2. plugins 注册的顺序是否有关系?看上去是个数组
  3. Fetch 内 runPluginHandler 内会根据 plugins 的注册进行调用,调用返回结果会统一存于一个大 object 内,所以这里后一个插件调用的结果值,有可能会覆盖前面的,是否比较容易出错?或者书写插件时比较小心翼翼?

一点点感悟

  • 代码组织可参考借鉴,以插件方式进行不同功能管理,易扩展
  • Class 与 Hooks 也可完美结合,有点意思
  • 对异步数据请求封装抽象出不同生命周期进行管理组织,清晰易懂