• 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, 小叨几句
      • 勾销 · 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)

抛开importtype, 这三十行代码实际上只有一句话:

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装进useLatest1以总是调用最新值, 并将返回的serviceRef作为入参在实例化Fetch时传入
  • 实例化后, 配置fetchInstanceoptions为组件入参中的options, 同时默认设置manulfalse
  • 实例化后, 配置fetchInstancepluginImpls为组件入参中的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的写法绑定后通过useMemoizedFn2返回以确保函数地址不变.

至此, 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. 浏览整个类的构造, 能够说是相当的清晰.

  • 状态治理: 几个很好了解的statesetState函数, 不另外解读了
  • 构造方法: 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, optionsmanual以外的值, 尽管在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结尾的事件, 而每到相应事件阶段, 都会别离调用用户从useRequestoptions中传入的事件办法 和 所有插件中的事件办法. 从中不难留神到, 申请前的onBefore事件时是先遍历调用插件中的事件后调用用户传入的事件, 申请后的三个事件反之. 集体对此其实不太理解, 浅显的了解了一下, 认为用户的事件在此总是比插件间隔申请自身更近, 即用户具备最实在资料(即申请自身)的最终决定和第一个应用的权力, 插件的权限次之. 将事件跨度再拉大一些, 其实也是让用户的事件办法间隔useRequest的出入参更远——试想, 刚拿到入参就做onBefore解决, 为什么不先解决了再给入参呢?

好了, 外围申请逻辑都在下面. 接下来看看返回给用户的六个办法. 当然, 虽说有六个, 实则接下来要聊的只有四个, 因为其中runrunAsync曾经读过了.

勾销 · 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...


  1. useLatest: ahooks 中与 useRequest 同一目录层级的 hook, 封装了 useRef. 返回以后最新值的 Hook, 能够防止闭包问题. https://ahooks.js.org/zh-CN/h... ↩
  2. useMemoizedFn: ahooks 中与 useRequest 同一目录层级的 hook, 基于 useMemo 和 useRef 进行封装. 长久化 function 的 Hook, 实践上, 能够应用 useMemoizedFn 齐全代替 useCallback. 能够省略第二个参数 deps, 同时保障函数地址永远不会变动. https://ahooks.js.org/zh-CN/h... ↩