ahooks - useRequest 源码阅读(一)

444 阅读6分钟

1. 找到源码

1.1 进入官网

ahooks - useRequest

image.png

1.2 从官网到 Github

image.png

1.3 github1s

使用 github1s 查看源码

在 url 中的 github 后面加上一个 1s 然后回车

image.png

1.4 ctrl + p

使用ctrl + p 快速定位到 useRequest hook

image.png

2. useRequest 的整体架构

2.1 plugin

  • 定位到 useRequest 目录以后可以看到,我们使用的 useRequest hook 来源于 src/useRequest.ts 文件
  • useRequest.ts 文件的顶部导入了很多的 use...Plugin 方法(或者也可以说是 hooks)。这些 plugin 的命名与 useRequest 所提供的功能是具有对应关系的(具体有哪些功能可以查看官网)。这就是 useRequest hook 架构中的第一个重点 —— 将功能拆分成 plugin,实现代码的解耦,方便功能的插拔和扩展

image.png

2.2 生命周期钩子

  • 我们可以大概的看一下每个 plugin 的内部结构,会发现所有的 plugin 方法都会返回一个对象,这个对象的属性可能是 onBefore、onRequest、onSuccess、onError、onFinally、onCancel、onMutate 中的一个或多个。这就是 useRequest hook 架构中的第二个重点 —— 基于生命周期的执行流程

image.png

2.3 Fetch 对象

  • 继续往下看,useRequest hook 的内部只调用了 useRequestImplement 方法,并且将所有的 plugin 都放在一个数组中传递进去
  • 进入 useRequestImplement 方法,在其顶部引用了很多 ahooks 中的其他 hook,这些 hook 的分析可以查看我的另外一篇博客。useMount 就是在 useEffect 中直接执行一个函数,useUnmount 就是在 useEffect 的返回值中执行
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);
    }
  });
  • 上面这段代码的意思就是,如果没有设置 manual 选项,那么 useRequest 会帮我们自动发一次请求。发请求时执行的是 fetchInstancerun 方法。那我们将重点放到 fetchInstance 这个对象是怎么生成的
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 对象是通过实例化 Fetch 对象得来的。这就是 useRequest hook 的第三个重点 —— useRequest 的执行核心 Fetch 对象 。我们所调用的 run、runAsync、refresh、cancel 等方法都来源于 Fetch 对象

image.png

2.4 小总结

  • 总结一下,useRequest 的整体架构就是 —— 将功能拆分成单独的 plugin;调用 plugin 后返回生命周期钩子函数;在 Fetch 实例对象内部(重点是 runAsync 方法)调用相应的生命周期钩子从而实现相应的功能。

image.png

3. 发送第一个请求

在了解了 useRequest 的整体架构以后,让我们来看看它是怎么样帮我们发送一个网络请求的

3.1 深入 useRequestImplement

1. 执行 plugin 身上的 onInit() 方法

  • 在创建 fetchInstance 对象时,会首先遍历所有的 plugin。如果 plugin 身上有 onInit() 方法,就会去执行它并且将所有的执行结果放到一个数组中,最后返回数组项为真值的元素
const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
  • 就目前所有的 plugin 来看,只有 useAutoRunPlugin 身上具有 onInit 方法
  • 该方法接收 readymanual 作为参数,返回一个属性为 loading,值为布尔值的对象
  • 所以 initState 的值为 [{ loading: true / false }]

2. 实例化 Fetch 对象

  • 接着往下看就是通过 new 操作符调用 Fetch 类,得到 fetchInstance 对象

3. 执行所有的 plugin

fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
  • 在这里会去执行所有的 plugin 函数,并将所有的返回值放到一个数组中,最终赋值给 fetchInstance 对象的 pluginImpls 属性

4. 执行 run() 方法

  • 如果在使用 useRequest 方法时,没有设置 manual 选项,那么会自动调用 fetchInstance.run()方法
  • 接下来就是将重点放到 Fetch 类中,看一看在调用 run() 方法时都做了什么事

3.2 Fetch 类

1. run() 和 runAsync() 的区别

思考一个问题:不管以什么形式调用 run() 方法,都不会拿到任何的返回值。但是如果在 Async 函数中调用 runAsync() 方法时,就可以拿到请求的返回值。这是怎么实现的呢?

run(...params: TParams) {
    this.runAsync(...params).catch((error) => {
      if (!this.options.onError) {
        console.error(error);
      }
    });
  }
  • 在调用 run() 时,其内部也是调用的 runAsync() 方法,那我们看一下 runAsync() 的大概结构
async runAsync(...params: TParams): Promise<TData> {
    // *** 省略部分代码 ***
    try {
        const res = await servicePromise;
        
        returen res;
    } cache (error) {
        // *** 省略部分代码 ***
        
        return error
    }
}
  • 由于 runAsync() 是一个 Async 函数,其返回值必然是一个 Promise 对象
  • 但是在 run() 方法中并没有使用 .then() 方法用来接收执行成功的结果,所以我们在调用 run() 方法时,永远也不会得到返回值。但是在使用 Async 函数调用 runAsync() 时是可以直接拿到请求结果的

2. runAsync()

// runAsync 中和生命周期钩子函数相关的主要代码,不是完整的源码
async runAsync(...params: TParams): Promise<TData> {
    // 调用内部的 onBefore 钩子
    const {
      stopNow = false,
      returnNow = false,
      ...state
    } = this.runPluginHandler('onBefore', params);
    
    try {
        // 调用内部的 onRequest 钩子
        let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);
        
        // 调用用户自定义的 onSuccess 钩子
        this.options.onSuccess?.(res, params);
        // 调用内部的 onSuccess 钩子
        this.runPluginHandler('onSuccess', res, params);

        // 调用用户自定义的 onFinally 钩子
        this.options.onFinally?.(params, res, undefined);
    } catch (error) {
        // 调用用户自定义的 onError 钩子
        this.options.onError?.(error, params);
        // 调用内部的 onError 钩子
        this.runPluginHandler('onError', error, params);

        // 调用用户自定义的 onFinally 钩子
        this.options.onFinally?.(params, undefined, error);

        if (currentCount === this.count) {
          // 调用内部的 onFinally 钩子
          this.runPluginHandler('onFinally', params, undefined, error);
        }
    }
}
  • 通过上面的代码可以发现,在 runAsync() 中会去调用源码内部的 onBefore、onRequest、onSuccess、onError 以及 onFinally 生命周期钩子。同时也会去调用用户自己定义的 onSuccess 和 onError 生命周期函数

3. ready 选项

在使用 useRequest 时,如果设置了 ready 选项的值为 false,请求永远不会发出。这是怎么实现的呢?

  • 通过上面的分析,可以很清晰的看到,在 runAsync() 内部其实就是在依次调用内部和用户自定义的生命周期钩子函数
  • useAutoRunPlugin 执行完成以后会返回一个 onBefore 钩子
const useAutoRunPlugin: Plugin<any, any[]> = () => {
    // *** 省略部分代码 ***
 
    return {
     onBefore: () => {
      if (!ready) {
        return {
          stopNow: true,
        };
      }
    },
  };
}
  • 在发送请求之前会去执行所有的 onBefore,如果 ready 选项为 false 时,runAsync() 函数就会立即返回
async runAsync(...params: TParams): Promise<TData> {
    const {
      stopNow = false,
      returnNow = false,
      ...state
    } = this.runPluginHandler('onBefore', params);
    
    // stop request
    if (stopNow) {
      return new Promise(() => {});
    }
}

4. 发送请求

  • 其中的 onRequest 就是实际去发送请求,只有 useCachePlugin 的返回值中有该方法
  • 单从发送请求的角度来看,这个执行流程比较简单,就是简单的函数调用
  • useRequest 的所有功能都是在执行生命周期函数的过程中一点一点扩展的,后面的文章将深入的看看这些功能是如何实现的