来一个 useRequest 大大大大大大分析

867 阅读27分钟

源码地址

先搞清楚几个点

为什么需要

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

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

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

import sampleApi from '???'
import { useEffect, useState } from 'react'

const Home = () => {
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState()
  const [data, setData] = useState()
  
  const init = async () => {
    setLoading(true)
    try {
      const result = await sampleApi()
      setData(result)
    } catch (error) {
      setError(error)
    } finally {
      setLoading(false)
    }
  }
  
  useEffect(() => { init() }, [])
  
  if (loading) return 'loading'
  if (error) return 'sorry, error'
  return <div>{JSON.stringify(data)}</div>
}
export default Home

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

import sampleApi from '???'

const Home = () => {
  const { loading, error, data } = useRequest(sampleApi)
  
  if (loading) return 'loading'
  if (error) return 'sorry, error'
  return <div>{JSON.stringify(data)}</div>
}
export default Home

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

不仅如此

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

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

因此不仅仅是针对一个异步操作过程的简单封装,甚至可以丰富更多的功能,比如 useRequest 提供的:

与具体请求库无关

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

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

整体架构

  • 暴露对外的 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 延迟 / 错误重试 / 屏幕重新请求:

插件初始化

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

插件生命周期

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


TS 类型定义

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

useRequest 入参部分

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

// hooks 的入参
// TData 异步操作后的返回值,TParams 异步操作的入参
function useRequest<TData, TParams extends any[]>(
  service: Service<TData, TParams>, // 异步操作
  options?: Options<TData, TParams>, // options
  plugins?: Plugin<TData, TParams>[], // 插件
)
  • services: 异步操作
// 入参是 TParams,返回值是 Promise<TData>,是个异步函数
type Service<TData, TParams extends any[]> = (...args: TParams) => Promise<TData>;
  • options: 功能 options
// 以下所有参数都是可选的,说明都是可有可无的,都是附加功能选项而已
interface Options<TData, TParams extends any[]> {
  // 是否开启人工触发,区别就是第一次加载之后,会不会自动给你触发异步请求
  // 默认是会给你自动触发的,不需要的话需要自行 false 关闭
  manual?: boolean;

  // 这几个都是用户可以订阅的异步请求生命周期函数(对应上面图 run / runAsync 内蓝色部分标记)
  onBefore?: (params: TParams) => void;
  onSuccess?: (data: TData, params: TParams) => void;
  onError?: (e: Error, params: TParams) => void;
  // formatResult?: (res: any) => TData;
  onFinally?: (params: TParams, data?: TData, e?: Error) => void;

  // 默认请求参数
  defaultParams?: TParams;

  // refreshDeps
  // 主要对应到的是 useAutoRunPlugin 插件 hooks 内的功能
  refreshDeps?: DependencyList;
  refreshDepsAction?: () => void;
  ready?: boolean;

  // loading delay
  // 主要对应到的是 useLoadingDelayPlugin 插件 hooks 内的功能
  loadingDelay?: number;

  // polling
  // 主要对应到的是 usePollingPlugin 插件 hooks 内的功能
  pollingInterval?: number;
  pollingWhenHidden?: boolean;

  // refresh on window focus
  // 主要对应到的是 useRefreshOnWindowFocusPlugin 插件 hooks 内的功能
  refreshOnWindowFocus?: boolean;
  focusTimespan?: number;

  // debounce
  // 主要对应到的是 useDebouncePlugin 插件 hooks 内的功能
  debounceWait?: number;
  debounceLeading?: boolean;
  debounceTrailing?: boolean;
  debounceMaxWait?: number;

  // throttle
  // 主要对应到的是 useThrottlePlugin 插件 hooks 内的功能
  throttleWait?: number;
  throttleLeading?: boolean;
  throttleTrailing?: boolean;

  // cache
  //主要对应到的是 useCachePlugin 插件 hooks 内的功能
  cacheKey?: string;
  cacheTime?: number;
  staleTime?: number;
  setCache?: (data: CachedData<TData, TParams>) => void;
  getCache?: (params: TParams) => CachedData<TData, TParams> | undefined;

  // retry
  // 主要对应到的是 useRetryPlugin 插件 hooks 内的功能
  retryCount?: number;
  retryInterval?: number;
}
  • plugins: plugins 文档未公开使用,但可以看到一个 plugin 定义的规范是怎样的
// 这里是一个 Plugin 的定义
// 一个 Plugin = Plugin Hooks + onInit
type Plugin<TData, TParams extends any[]> = {
  // Plugin Hooks,props = 实例化的Fetch + 用户传入的 options
  // Plugin Hooks,return = 订阅异步过程的生命周期函数 list
  (fetchInstance: Fetch<TData, TParams>, options: Options<TData, TParams>): PluginReturn<
    TData,
    TParams
  >;
  // onInit,主要为获取 initState 使用
  onInit?: (options: Options<TData, TParams>) => Partial<FetchState<TData, TParams>>;
};


// Plugin Hooks 的返回值定义
// 订阅异步过程的生命周期函数 list(对应上面图 run / runAsync 内红色部分标记)
interface PluginReturn<TData, TParams extends any[]> {
  onBefore?: (params: TParams) =>
    | ({
        stopNow?: boolean;
        returnNow?: boolean;
      } & Partial<FetchState<TData, TParams>>)
    | void;

  onRequest?: (
    service: Service<TData, TParams>,
    params: TParams,
  ) => {
    servicePromise?: Promise<TData>;
  };

  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;
}

useRequest 返回值部分

interface Result<TData, TParams extends any[]> {
  // 异步过程中的几个状态 & 数据 & 参数
  loading: boolean;
  data?: TData;
  error?: Error;
  params: TParams | [];
  
  // 提供给到外部,可以直接执行的方法
  // 暴露的方法都是从 Class Fetch 内直接获取的,从这里就可以看出 Fetch 的作用了
  cancel: Fetch<TData, TParams>['cancel'];
  refresh: Fetch<TData, TParams>['refresh'];
  refreshAsync: Fetch<TData, TParams>['refreshAsync'];
  run: Fetch<TData, TParams>['run'];
  runAsync: Fetch<TData, TParams>['runAsync'];
  mutate: Fetch<TData, TParams>['mutate'];
}

源码分析

useRequest Main 流程

function useRequestImplement<TData, TParams extends any[]>(
  service: Service<TData, TParams>,
  options: Options<TData, TParams> = {},
  plugins: Plugin<TData, TParams>[] = [],
) {
  // 1. 默认参数处理过程,如果 manual 没有传的话,默认处理成 false
  const { manual = false, ...rest } = options;
  const fetchOptions = {
    manual,
    ...rest,
  };

  // 2. 异步函数使用 useLatest 进行存储
  // https://ahooks.js.org/zh-CN/hooks/use-latest
  // 作用主要是保持最新值的 service,useLatest 内部使用 useRef 进行存储
  const serviceRef = useLatest(service);
    
  // 3. 生成强制组件渲染函数,提供给到 Fetch 使用的
  // https://ahooks.js.org/zh-CN/hooks/use-update
  const update = useUpdate();

  // 4. Fetch 实例化过程
  const fetchInstance = useCreation(() => {
    // 4.1 InitState,逐个 plugin 获取 state,进行合并
    const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);

    // 4.2 Fetch 实例化
    return new Fetch<TData, TParams>(
      serviceRef,
      fetchOptions,
      update,
      Object.assign({}, ...initState),
    );
  }, []);
  fetchInstance.options = fetchOptions;
  // 5. 注册&执行插件过程,逐一调用并且存至 fetchInstance 的 pluginImpls 内
  // run all plugins hooks
  fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));

  // 第一次加载后,针对 manual = false 的自动 run 处理
  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);
    }
  });

  // 卸载的时候,调用 fetchInstance 的 cancel 函数进行销毁即可
  useUnmount(() => {
    fetchInstance.cancel();
  });

  // hooks 导出的,实则全是 fetchInstance 内的值 或者 方法,所以关键主要看 Fetch 的实现
  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>;
}

Class Fetch

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

export default class Fetch<TData, TParams extends any[]> {
  // 保存所有的 plugin hooks
  pluginImpls: PluginReturn<TData, TParams>[];

  // 计时器,执行了多少遍
  count: number = 0;

  // 异步数据管理
  // loading:是否正在执行
  // params:请求参数
  // data:返回值
  // error:错误时候的值
  state: FetchState<TData, TParams> = {
    loading: false,
    params: undefined,
    data: undefined,
    error: undefined,
  };

  constructor(
    // 用户传进来的异步操作,并且经过 useLatest hooks 导出的 ref 最新值
    public serviceRef: MutableRefObject<Service<TData, TParams>>,
    // 用户传进来的 options
    public options: Options<TData, TParams>,
    // 订阅操作,这里主要传了 useUpdate,强制组件刷新函数进来
    public subscribe: Subscribe,
    // 所有插件的 initState 合集(遍历各个插件的 onInit 方法后,得到的值,合并后得来的)
    public initState: Partial<FetchState<TData, TParams>> = {},
  ) {
    // 整一个大合并,到 state
    this.state = {
      ...this.state,
      // 如果不需要手工执行,那么实例化的时候就是 loading 中的
      // 如果需要手工执行,那么实例化的时候就不是 loading 中的
      loading: !options.manual,
      ...initState,
    };
  }

  // 合并 state 操作,再触发订阅函数即可
  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> { ... }
  run(...params: TParams) { ... }
  
  // 一个取消方法
  cancel() { ... }
  
  // 两个刷新的方法
  refresh() { ... }
  refreshAsync() { ... }
  
  // 一个立即修改 data 数据的方法
  mutate(data?: TData | ((oldData?: TData) => TData | undefined)) { ... }
}

再展开讲讲:

  • run / runAsnyc(对应流程图 run / runAsync):
async runAsync(...params: TParams): Promise<TData> {
  // 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) {
      servicePromise = this.serviceRef.current(...params);
    }
    // 7.2 执行 service
    const res = await servicePromise;
    // ---执行部分---------------------------------------------

    
    // 下面这段开始,其实跟 catch error 逻辑大部分类似的
    // ---执行成功部分---------------------------------------------
    // 判断是不是被 cancel 了
    if (currentCount !== this.count) {
      // prevent run.then when request is canceled
      return new Promise(() => {});
    }

    // 设置新的 state,error 要清空
    this.setState({
      data: res,
      error: undefined,
      loading: false,
    });

    // 先执行用户的 onSuccess 生命周期钩子函数
    this.options.onSuccess?.(res, params);
    // 再插件的 onSuccess 生命周期钩子函数
    this.runPluginHandler('onSuccess', res, params);

    // 先执行用户的 onFinally 生命周期钩子函数
    this.options.onFinally?.(params, res, undefined);
    // 再插件的 onFinally 生命周期钩子函数
    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(() => {});
    }

    // 设置新的 state,data 不会覆盖上次的
    this.setState({
      error,
      loading: false,
    });

    // 先执行用户的 onError 生命周期钩子函数
    this.options.onError?.(error, params);
    // 再插件的 onError 生命周期钩子函数
    this.runPluginHandler('onError', error, params);

    // 先执行用户的 onFinally 生命周期钩子函数
    this.options.onFinally?.(params, undefined, error);
    // 再插件的 onFinally 生命周期钩子函数
    if (currentCount === this.count) {
      this.runPluginHandler('onFinally', params, undefined, error);
    }

    throw error; // 抛出异常
    // ---执行失败部分---------------------------------------------
  }
}

// 实际只是调用 runAsync
// 作用就是在 useRequest 内部进行 catch 住 error,不对外抛
// 如果使用 runAsync 的话,就需要用户自己 catch 住错误
run(...params: TParams) {
  this.runAsync(...params).catch((error) => {
    if (!this.options.onError) {
      console.error(error);
    }
  });
}
  • cancel:(对应流程图 cancel)
  1. count + 1 处理
    1. 事实上判断当前请求是不是被取消,是通过这个 count 去判断的,对应到 runAsync 内有个判断(currentCount !== this.count 的话,就直接返回空的 Promise)
    2. cancel 过程是同步的,异步过程是异步的,所有就能判断到不一致的情况
    3. 这里要注意的是,异步过程终究还是执行了,不是真的对这个异步过程取消执行,只是这个过程执行了,但不需要返回而已
  2. 设置 state 的 loading 为 false
  3. 执行插件的 onCanel 钩子函数
cancel() {
  this.count += 1;
  this.setState({
    loading: false,
  });

  this.runPluginHandler('onCancel');
}
  • refresh / refreshAsync(对应流程图 refresh / refreshAsnyc): 只是对 run 进行包装一层,用的是上次的存储的 params 进行重刷
refresh() {
  // @ts-ignore
  this.run(...(this.state.params || []));
}

refreshAsync() {
  // @ts-ignore
  return this.runAsync(...(this.state.params || []));
}
  • mutate: 立即执行更新内部 data 数据,并且触发 plugins 的 onMutate 生命周期钩子函数
mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
  let targetData: TData | undefined;
  if (typeof data === 'function') {
    // @ts-ignore
    targetData = data(this.state.data);
  } else {
    targetData = data;
  }

  this.runPluginHandler('onMutate', targetData);

  this.setState({
    data: targetData,
  });
}

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 钩子函数
// support refreshDeps & ready
const useAutoRunPlugin: Plugin<any, any[]> = (
  fetchInstance,
  { manual, ready = true, defaultParams = [], refreshDeps = [], refreshDepsAction },
) => {
  const hasAutoRun = useRef(false);
  hasAutoRun.current = false; // 每次组件渲染都会初始化为 false

  useUpdateEffect(() => {
    // 如果 ready 并且不需要人工触发的话,则需要自动执行 run
    if (!manual && ready) {
      hasAutoRun.current = true;
      // 开启 ready 的话,使用 defaultParams 执行 run
      fetchInstance.run(...defaultParams);
    }
  }, [ready]);

  useUpdateEffect(() => {
    // 说明正在执行上面的 run
    if (hasAutoRun.current) {
      return;
    }
    
    // 这里只判断了 manual 的情况,没有把 ready 参数带进来,本质有没有应该无所谓?
    // 如果有 ready 判断,refresh 操作执行可以忽略
    // 目前来看的话,其实每次 refreshDeps 变化,都会执行到 refresh
    if (!manual) {
      hasAutoRun.current = true;
      
      // 如果有传递 refreshDepsAction 操作函数,则不会执行 fetch 的刷新操作
      if (refreshDepsAction) {
        refreshDepsAction();
      } else {
        fetchInstance.refresh();
      }
    }
  }, [...refreshDeps]);

  return {
    // 订阅 onBefore 钩子,返回 stopNow 标志
    onBefore: () => {
      // 如果 ready 为 false,则设置 stopNow 标志进行流程中断
      if (!ready) {
        return {
          stopNow: true,
        };
      }
    },
  };
};

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

useDebouncePlugin / useThrottlePlugin

防抖 / 节流,两个处理操作差不多,一起讲
主要处理 options 内 debounceWait / debounceLeading / debounceTrailing / debounceMaxWait / throttleWait / throttleLeading / throttleTrailing

  • debounce 依赖 lodash/debounce 功能,throttle 依赖 lodash / throttle 功能
  • 对 options 进行重新格式化,转成 lodash 内置函数参数
  • 防抖 / 节流,针对 fetchInstance.runAsync 函数进行重写,加一层 debounce / throttle 函数进行执行,当组件销毁时,取消 debounce / throttle,并且对 fetchInstance.runAsync 函数进行还原
  • 如果开启防抖 / 节流功能,需要订阅 onCanel 钩子函数,进行 取消 debounce / throttle
const useDebouncePlugin: Plugin<any, any[]> = (
  fetchInstance,
  { debounceWait, debounceLeading, debounceTrailing, debounceMaxWait },
) => {
  const debouncedRef = useRef<DebouncedFunc<any>>();

  // 1. 参数格式化
  const options = useMemo(() => {
    const ret: DebounceSettings = {};
    if (debounceLeading !== undefined) {
      ret.leading = debounceLeading;
    }
    if (debounceTrailing !== undefined) {
      ret.trailing = debounceTrailing;
    }
    if (debounceMaxWait !== undefined) {
      ret.maxWait = debounceMaxWait;
    }
    return ret;
  }, [debounceLeading, debounceTrailing, debounceMaxWait]);

  // 2. 如果 debounceWait, options 参数变化,就触发副作用函数
  useEffect(() => {
    if (debounceWait) {
      // 保存原来的 fetchInstance.runAsync
      const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);

      debouncedRef.current = debounce(
        (callback) => {
          callback();
        },
        debounceWait,
        options,
      );

      // 改写 fetchInstance.runAsync,加一层防抖或者节流,在回调内去执行真正的 runAsync
      // debounce runAsync should be promise
      // https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
      fetchInstance.runAsync = (...args) => {
        return new Promise((resolve, reject) => {
          debouncedRef.current?.(() => {
            _originRunAsync(...args)
              .then(resolve)
              .catch(reject);
          });
        });
      };

      // 销毁时,取消节流 / 防抖函数并且还原回原来的 fetchInstance.runAsync 函数
      return () => {
        debouncedRef.current?.cancel();
        fetchInstance.runAsync = _originRunAsync;
      };
    }
  }, [debounceWait, options]);

  // 如果没有开启防抖 / 节流,则不需要返回生命周期钩子函数
  if (!debounceWait) {
    return {};
  }

  // 如果开启防抖 / 节流,需要订阅 onCanel 的钩子函数
  // 主要为了取消防抖 / 节流
  return {
    onCancel: () => {
      debouncedRef.current?.cancel();
    },
  };
};

useThrottlePlugin 类似,不另外赘述

useLoadingDelayPlugin

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

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

  if (!loadingDelay) {
    return {};
  }

  const cancelTimeout = () => {
    if (timerRef.current) {
      clearTimeout(timerRef.current);
    }
  };

  return {
    onBefore: () => {
      cancelTimeout();

      // 延迟设置 loading = true
      timerRef.current = setTimeout(() => {
        fetchInstance.setState({
          loading: true,
        });
      }, loadingDelay);

      // 先改写成 false
      return {
        loading: false,
      };
    },
    onFinally: () => {
      cancelTimeout(); // 取消定时器
    },
    onCancel: () => {
      cancelTimeout(); // 取消定时器
    },
  };
};

useRetryPlugin

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

整体重试流程可以概括为以下的流程图: image.png

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

  const triggerByRetry = useRef(false);

  // 如果没有开启重试功能,不需要返回钩子函数
  if (!retryCount) {
    return {};
  }

  return {
    onBefore: () => {
      // 说明这是人为导致的异步触发,需要重置 count
      // 因为重试执行的异步过程 triggerByRetry 为 true
      // 可以看到 setTimeout 里面 refresh 之前,设置了 triggerByRetry = true
      if (!triggerByRetry.current) {
        countRef.current = 0;
      }
      
      // 每次请求前,都会重置 triggerByRetry 标志为 false
      // 并且清除已有的定时器
      // 因为有可能在定时器还没到时间之前,人为进行触发了异步,那么之前在等待重试的异步过程,需要清掉
      triggerByRetry.current = false;
      if (timerRef.current) {
        clearTimeout(timerRef.current);
      }
    },
    onSuccess: () => {
      // onSuccess 这里只是 count 清零,没有清除已有的定时器,是否有问题?
      // 正在 setTimeout 等待重试的过程中,用户手动点击重刷异步操作,并且成功了,这个时候应该是要清除的?
      countRef.current = 0;
    },
    onError: () => {
      countRef.current += 1;
      if (retryCount === -1 || countRef.current <= retryCount) {
        // Exponential backoff
        const timeout = retryInterval ?? Math.min(1000 * 2 ** countRef.current, 30000);
        timerRef.current = setTimeout(() => {
          triggerByRetry.current = true; // 调用在重试之前,会将 triggerByRetry 设置为 true
          fetchInstance.refresh(); // 调用刷新
        }, timeout);
      } else {
        countRef.current = 0;
      }
    },
    onCancel: () => {
      countRef.current = 0;
      if (timerRef.current) {
        clearTimeout(timerRef.current);
      }
    },
  };
};

usePollingPlugin

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

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

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

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

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

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

      // 设置轮询
      timerRef.current = setTimeout(() => {
        fetchInstance.refresh();
      }, pollingInterval);
    },
    onCancel: () => {
      stopPolling();
    },
  };
};

对于标签页激活,实现了一个简单的发布-订阅:

const listeners: any[] = []; // 全局保存了回调操作 list

// 事件订阅
function subscribe(listener: () => void) {
  listeners.push(listener); // 放于 list 内
  
  // 返回退订函数,退订函数内知识讲该回调剔除出 list 之外
  return function unsubscribe() {
    const index = listeners.indexOf(listener);
    listeners.splice(index, 1);
  };
}

if (canUseDom()) {
  // visibilitychange 事件,当前如果隐藏,则不操作
  // 当前如果是激活,则遍历 listeners,逐个回调依次执行
  const revalidate = () => {
    if (!isDocumentVisible()) return;
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }
  };
  
  // 监听 visibilitychange 事件
  window.addEventListener('visibilitychange', revalidate, false);
}

useRefreshOnWindowFocusPlugin

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

  • 当 refreshOnWindowFocus = true 时,订阅 focus 事件,当 focus 时,重试执行异步的 refresh 操作,这里的 limit 函数限制了执行间隔,可以看到具体的实现
  • 在组件销毁 / refreshOnWindowFocus 变为 false 时,注销订阅事件
  • 具体 subscribeFocus 实现,与上面 subscribeReVisible 实现类似,全局管理了一个 listeners 队列,回调后遍历执行
const useRefreshOnWindowFocusPlugin: Plugin<any, any[]> = (
  fetchInstance,
  { refreshOnWindowFocus, focusTimespan = 5000 },
) => {
  const unsubscribeRef = useRef<() => void>();

  // 注销
  const stopSubscribe = () => {
    unsubscribeRef.current?.();
  };

  useEffect(() => {
    // refreshOnWindowFocus = true 时候订阅执行
    if (refreshOnWindowFocus) {
      const limitRefresh = limit(fetchInstance.refresh.bind(fetchInstance), focusTimespan);
      unsubscribeRef.current = subscribeFocus(() => {
        limitRefresh();
      });
    }
    
    // 注销
    return () => {
      stopSubscribe();
    };
  }, [refreshOnWindowFocus, focusTimespan]);

  // 注销
  useUnmount(() => {
    stopSubscribe();
  });

  // 不需要监听任何生命周期,因为这是个一次性的操作,与任何请求阶段无关
  return {};
};

用 pending 标志控制是否执行,pending 为 false 的时候执行 fn,当 timespan 后,再设置会标志为 pending 为 true,因此在 timespan 内 pending = true,都会被直接 return 掉。以此来控制 fn 执行的时间间隔。

export default function limit(fn: any, timespan: number) {
  let pending = false;
  return (...args: any[]) => {
    if (pending) return;
    pending = true;
    fn(...args);
    setTimeout(() => {
      pending = false;
    }, timespan);
  };
}

useCachePlugin

SWR,数据缓存能力
主要处理 options 内 cacheKey / cacheTime / staleTime / setCache / getCache

数据新鲜

先查看 utils/cache 代码,实现了一个简单的内存 cache 操作:

  • 全局保存了 cache Map,以 cacheKey 作为 key,对应 value 存储了 data - 异步返回结果,params - 请求参数,time - 清除缓存定时器(默认 cacheTime = 5min,plugin hooks 默认参数设置)
  • setCache,首先对已有 key 的 cache 进行清除(主要是清除计时器),再针对这个 key 进行重新注册
  • 同时暴露 getCache 与 clearCache 操作,对应实现也比较简单
export interface CachedData<TData = any, TParams = any> {
  data: TData; // 异步执行结果
  params: TParams; // 异步请求参数
  time: number; // 缓存那一刻的时间
}
interface RecordData extends CachedData {
  timer: Timer | undefined; // 清除缓存定时器
}

// map 存储数据,全局一份
const cache = new Map<CachedKey, RecordData>();

// 设置key 对应的 cache
const setCache = (key: CachedKey, cacheTime: number, cachedData: CachedData) => {
  // 先清除
  const currentCache = cache.get(key);
  if (currentCache?.timer) {
    clearTimeout(currentCache.timer);
  }

  // 再重新登记
  let timer: Timer | undefined = undefined;

  // 如果是 -1 的话,timer 为空,永不清除
  if (cacheTime > -1) {
    // if cache out, clear it
    timer = setTimeout(() => {
      cache.delete(key);
    }, cacheTime);
  }

  cache.set(key, {
    ...cachedData,
    timer,
  });
};

// 获取对应 cache
const getCache = (key: CachedKey) => {
  return cache.get(key);
};

// 清除某些 cache,可以一次清除多个或者一个,不传就是全清除
const clearCache = (key?: string | string[]) => {
  if (key) {
    const cacheKeys = Array.isArray(key) ? key : [key];
    cacheKeys.forEach((cacheKey) => cache.delete(cacheKey));
  } else {
    cache.clear();
  }
};

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

  1. 没有命中数据缓存的情况下:需要等到异步数据返回,才能展示数据

    • loading:false -> true
    • data:无 -> 新
    • onBefore: 不拦截请求,返回空数据即可
    • onSuccess: 如果设置了 cacheKey,则设置
    • loading && !data 联合判断下:
      • loading 状态会显示出来
      • 异步结束时,展示数据
  2. 命中数据缓存,在 cacheTime 时间内,但不在 slateTime 时间内:先从 cache 获取展示旧 data,不阻塞异步操作,等异步数据回来后,再直接更新展示新 data

    • loading:false -> true
    • data:旧 -> 新
    • onBefore:在请求前,先从 cache 获取数据,先写到 data 里面,loading 和 params 还是正常请求的状态值
    • onSuccess: 如果设置了 cacheKey,则设置
    • hooks 初始化时: 命中缓存的情况下(缓存时间不在 slateTime 内),会从 cache 内获取数据先直接写入 params 和 data
    • loading && !data 联合判断下:
      • loading 状态不会显示,会默认展示到上一次的数据
      • 异步结束后,直接重新渲染新的数据
  3. 命中数据缓存,并且在 slateTime 时间内:不从异步操作获取数据,直接从 cache 获取

    • loading:始终 false
    • data:无 / 旧 -> 新
    • onBefore: slateTime 等于 -1 说明永远新鲜,或者 cache 时间间隔在 slateTime 内,则直接从 cache get 数据,返回 returnNow = true 标志停止异步过程
    • onSuccess:onBefore 直接 return 掉了,不会走到这个钩子的
    • hooks 初始化时: 在 slateTime 时间内,不需要展示 loading 状态(大框框的逻辑命中缓存时间内都会走到,小框框是只有 slateTime 时间内才会走,区别只是需不需要设置 loading 状态而已)
    • loading && !data 联合判断下:
      • loading 状态不会显示,会默认展示到上一次的数据
      • 上一次的数据,就是最新的数据

总结来说,具体缓存时间可以以下概括:

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

数据共享

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

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

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

utils / cachePromise 实现了对一个 Promise 的缓存:

type CachedKey = string | number;

// 缓存 Map,key 是 cacheKey
const cachePromise = new Map<CachedKey, Promise<any>>();

// 读取 promise cache
const getCachePromise = (cacheKey: CachedKey) => {
  return cachePromise.get(cacheKey);
};

// 写入 promise cache
const setCachePromise = (cacheKey: CachedKey, promise: Promise<any>) => {
  // Should cache the same promise, cannot be promise.finally
  // Because the promise.finally will change the reference of the promise
  cachePromise.set(cacheKey, promise);

  // no use promise.finally for compatibility
  // 在 promise 结束的时候,then 和 cache 再删除 cache 即可
  promise
    .then((res) => {
      cachePromise.delete(cacheKey);
      return res;
    })
    .catch(() => {
      cachePromise.delete(cacheKey);
    });
};

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

// 关注 useCachePlugin 内的 onRequest:
const useCachePlugin: Plugin<any, any[]> = (
  fetchInstance,
  {
    cacheKey,
    cacheTime = 5 * 60 * 1000,
    staleTime = 0,
    setCache: customSetCache,
    getCache: customGetCache,
  },
) => {
  const currentPromiseRef = useRef<Promise<any>>();

  // ...

  return {
    onBefore: (params) => {
      // ...
    },
    onRequest: (service, args) => {
      // 1: 先从 cache 读 promise
      let servicePromise = cachePromise.getCachePromise(cacheKey);

      // 2.1: 如果有,则直接返回这个 promise
      if (servicePromise && servicePromise !== currentPromiseRef.current) {
        return { servicePromise };
      }

      // 2.2: 如果没有,则构造一个缓存,然后返回这个 promise
      servicePromise = service(...args);
      currentPromiseRef.current = servicePromise;
      cachePromise.setCachePromise(cacheKey, servicePromise);
      return { servicePromise };
    },
    onSuccess: (data, params) => {
      // ...
    },
    onMutate: (data) => {
      // ...
    },
  };
};



// Fetch 的 runAsync 操作:
async runAsync() {
  try {
      // 1. 从 onRequest 钩子拿到 servicePromise
      // 有可能是缓存的,也有可能不是,不是的话会顺便缓存一个
      let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);

      // 2. 兜底空的情况
      if (!servicePromise) {
        servicePromise = this.serviceRef.current(...params);
      }

      // 3. 执行这个 promise 获取数据
      const res = await servicePromise;
    
      // ...
  }
}

针对第二点,数据的共享,大致流程图如下,假如 ComponentA 是被触发异步操作的,并且 ComponentB 渲染的数据与 ComponentA 是同一份(ComponentB 没有被触发异步操作):

可以先看下 utils/cacheSubscribe,实现了针对 cache key set 操作的发布-订阅:

type Listener = (data: any) => void;
// 事件队列
const listeners: Record<string, Listener[]> = {};

// 触发过程,就是遍历 listeners 逐个传参数执行
const trigger = (key: string, data: any) => {
  if (listeners[key]) {
    listeners[key].forEach((item) => item(data));
  }
};

const subscribe = (key: string, listener: Listener) => {
  if (!listeners[key]) {
    listeners[key] = [];
  }
  listeners[key].push(listener); // 订阅 push 函数到队列

  // 退订操作
  return function unsubscribe() {
    const index = listeners[key].indexOf(listener);
    listeners[key].splice(index, 1);
  };
};

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
const useCachePlugin: Plugin<any, any[]> = (
  fetchInstance,
  {
    cacheKey,
    cacheTime = 5 * 60 * 1000,
    staleTime = 0,
    setCache: customSetCache,
    getCache: customGetCache,
  },
) => {
  const unSubscribeRef = useRef<() => void>();

  // setCache 操作:会 trigger 队列里所有的事件
  const _setCache = (key: string, cachedData: CachedData) => {
    // ...
    cacheSubscribe.trigger(key, cachedData.data);
  };

  // ...

  useCreation(() => {
    // ...

    // 首次 hooks 注册订阅 cache set 事件
    // subscribe same cachekey update, trigger update
    unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (data) => {
      fetchInstance.setState({ data });
    });
  }, []);

  // 组件注销,退订 cache set
  useUnmount(() => {
    unSubscribeRef.current?.();
  });

  // ...
  return {
    onBefore: (params) => {
      // ...
    },
    onRequest: (service, args) => {
      // ...
    },
    onSuccess: (data, params) => {
      if (cacheKey) {
        // cancel subscribe, avoid trgger self
        unSubscribeRef.current?.(); // 1. 先注销自己
        // 2. 触发 set cache,set cache trigger 事件队列
        _setCache(cacheKey, {
          data,
          params,
          time: new Date().getTime(),
        });
        // resubscribe
        // 3. 重新订阅
        unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (d) => {
          fetchInstance.setState({ data: d });
        });
      }
    },
    // 类似 onSuccess 的步骤,不赘述
    onMutate: (data) => {
      if (cacheKey) {
        // cancel subscribe, avoid trgger self
        unSubscribeRef.current?.();
        _setCache(cacheKey, {
          data,
          params: fetchInstance.state.params,
          time: new Date().getTime(),
        });
        // resubscribe
        unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (d) => {
          fetchInstance.setState({ data: d });
        });
      }
    },
  };
};

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


细节分析

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

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

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

  • 本质还是使用 useRef,将 factory 函数执行的结果保存在 current.obj 内
  • 使用 initialized 标记是不是已经初始化过
import type { DependencyList } from 'react';
import { useRef } from 'react';
import depsAreSame from '../utils/depsAreSame';

export default function useCreation<T>(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;
}

Fetch 是 Class,数据也是存储在 Class 内的,如何通知外层 useRequest hooks 需要做组件更新?

使用的是 useUpdate:ahooks.js.org/zh-CN/hooks…,具体实现如下:其实是设置了个空的 state,每次都强行设置 state,就能触发更新,然后把这个 hooks 返回的函数,传至 Fetch 内进行使用

const useUpdate = () => {
  const [, setState] = useState({});

  return useCallback(() => setState({}), []);
};

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

// Fetch cancel 对 count 进行 + 1 处理
cancel() {
  this.count += 1;
  this.setState({
    loading: false,
  });

  this.runPluginHandler('onCancel');
}

// Fetch runAsync 执行后会判断 currentCount !== this.count
async runAsync() {
  // ...
  
  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(() => {});
      }
    
      // ...
  }
  
  // ...
}

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

// useRequest 内注册插件时
// 会把当前的实例化Fetch 与 全量 options 往下传递
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));

// useAutoRunPlugin
// 只接收了 manual / ready / defaultParams / refreshDeps / refreshDepsAction
const useAutoRunPlugin: Plugin<any, any[]> = (
  fetchInstance,
  { manual, ready = true, defaultParams = [], refreshDeps = [], refreshDepsAction },
) => { ... }

// useCachePlugin
// 只接收了 cacheKey / cacheTime / staleTime / setCache / getCache 
const useCachePlugin: Plugin<any, any[]> = (
  fetchInstance,
  {
    cacheKey,
    cacheTime = 5 * 60 * 1000,
    staleTime = 0,
    setCache: customSetCache,
    getCache: customGetCache,
  },
) => { ... }

// 等等以此类推

疑问点保留

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

一点点感悟

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