ahooks 源码解析之 useRequest

avatar

源码地址:github.com/alibaba/hoo…

前言

我们一看到 request 的字样自然就联想到 useRequest 一定是调后端接口的请求库,其实并不是。

它是一个异步数据管理的 Hooks,也就是说它是用来给异步方法丰富更多强大的功能的,它本身和远程数据请求库是解耦的,它可以配合 axios、原生fetch、甚至只是一个简单的 Promise 都是 OK 的。

个人觉得通过阅读 useRequest源码来入门学习 js 的插件和生命周期设计非常合适,源码本身也比较简单

源码解析

入口文件:

import useRequest from './src/useRequest';
import { clearCache } from './src/utils/cache';

export { clearCache };

export default useRequest;

useRequest 对外提供了两个方法,一个是方法本身,一个是 clearCache,它可以让使用者自行清除已缓存的请求数据

useRequst.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 可以分成三部分:

  1. useRequestImplement:用来生成最核心的 Fetch 方法及运行插件和生命周期
  2. plugins:以插件的形式实现各种功能
  3. service:外部传入的异步操作方法

useRequestImplement

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

  // useLatest hooks 可以获取到的 service 的最新值,避免闭包问题
  const serviceRef = useLatest(service);

  // 调用 update 方法可以强制刷新 react 组件
  const update = useUpdate();

  // useCreation 类似 useMemo
  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(() => {
    // 如果 manual 为 false,则直接执行 run 方法,发起异步操作
    if (!manual) {
      // useCachePlugin can set fetchInstance.state.params from cache when init
      const params = fetchInstance.state.params || options.defaultParams || [];
      fetchInstance.run(...params);
    }
  });

  useUnmount(() => {
    fetchInstance.cancel();
  });

  // 调用 useRequest 返回的方法对象
  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>;
}

掌握几个核心点:

  1. fetchInstance 为异步操作的核心对象,它返回了 Fetch 类的实例化对象(这个 Fetch 不是那个浏览器原生 Fetch 方法),并接受四个参数
  • serviceRef:异步操作方法
  • fetchOptions:请求的配置参数
  • update:强制更新 react 组件方法
  • initState:初始状态对象 → 通过执行每个 plugin 的 onInit 方法收集整合得到
  1. 执行每个 plugin 方法,传入 fetchInstancefetchOptions ,将执行结果赋值给 fetchInstance.pluginImpls
  2. 在组件首次渲染(useMount)时,如果没有设置 manual,则直接执行 fetchInstance.run 方法执行异步操作
  3. 在组件卸载(useUnmount)时,执行 fetchInstance.cancel 方法

Fetch

Fetch 是核心类,这里分成几个阶段来解析:

初始化

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

这里初始化了三个变量:

  1. pluginImpls:保存插件的回调数据对象
  2. count:计数器,具体用法后面会讲
  3. state:内部状态,初始化有 loading、params、data 和 error

同时接受四个参数,serviceRef/ options / subscribe / initState,具体含义上面 fetchInstance 里有讲到,不再赘述。

执行

无论 manual 是否设置,最终都以执行 run 方法为开始,其内部调用了 runAsync 方法传入请求参数 params

run(...params: TParams) {
  this.runAsync(...params).catch((error) => {
    if (!this.options.onError) {
      console.error(error);
    }
  });
}

下面的 runAsync 的源码部分,其中省略了插件逻辑和生命周期,后面会单独讲

async runAsync(...params: TParams): Promise<TData> {
  this.count += 1;
  const currentCount = this.count;

  this.setState({
    loading: true,
    params
  });

  try {
    const res = await this.serviceRef.current(...params);

    if (currentCount !== this.count) {
      // prevent run.then when request is canceled
      return new Promise(() => {});
    }

    this.setState({
      data: res,
      error: undefined,
      loading: false,
    });

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

    if (currentCount === this.count) {
      this.runPluginHandler('onFinally', params, undefined, error);
    }

    throw error;
  }
}

讲 runAsync 之前,我们先看下里面用到的一些方法都干了啥

// 设置 state,同时调用 subscribe 方法,触发视图组件更新
setState(s: Partial<FetchState<TData, TParams>> = {}) {
  this.state = {
    ...this.state,
    ...s,
  };
  // 这里的 subscribe 就是调用 Fetch 时传入的 update 方法
  this.subscribe();
}
// 执行所有插件中的 event 事件并收集返回对象,整合后输出
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
  const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);
  return Object.assign({}, ...r);
}

还有 this.countcurrentCount 的逻辑,我们看下 cancel 方法源码:

cancel() {
  this.count += 1;
  this.setState({
    loading: false,
  });

  this.runPluginHandler('onCancel');
}

当调用 cancel 方法时,this.count 会加 1。这样this.countcurrentCount 不再相等。

再回过头来看 runAsync 的逻辑就很清晰了

  1. 设置 loading 和 params,触发视图组件更新
  2. 执行 serviceRef 发出异步请求,接受返回结果
  3. 对比 this.countcurrentCount,如果不相等直接返回空 promise,不再执行后续逻辑,这也就实现了一个取消请求的操作,不过要注意的是真实请求依然会继续进行
  4. 设置 loading、data、error,执行 onFinally 插件事件回调

Fetch 里面还有一些其他方法,比如 refreshrefreshAsyncmutate 比较简单,看下源码就能理解。

小结

到此,useRequest 的核心逻辑就读完了,整体分为数据初始化和发送请求两部分。useRequestImplement 通过 hooks 把 react 视图 和 异步数据处理逻辑链接在一起。

下面我们再单独来看 useRequest 的插件和事件订阅是如何实现的。

插件

useRequestImplement(service, options, [
  ...(plugins || []),
  useDebouncePlugin,
  useLoadingDelayPlugin,
  usePollingPlugin,
  useRefreshOnWindowFocusPlugin,
  useThrottlePlugin,
  useAutoRunPlugin,
  useCachePlugin,
  useRetryPlugin,
]

我们可以看到,useRequest 以插件形式来实现各种能力,比如防抖、延迟 loading 等等,好处是使核心逻辑更简洁、各功能之间相互解耦可以任意组合。

功能实现

传入到 useRequestImplement 的插件方法会被依次执行,每个插件都会接受到 fetchInstancefetchOptions,可以返回一个对象。

// useRequestImplement.ts
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));

插件对象还可以通过 onInit 方法设置初始化 state

const fetchInstance = useCreation(() => {
  const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);

  return new Fetch<TData, TParams>(
    serviceRef,
    fetchOptions,
    update,
    Object.assign({}, ...initState),
  );
}, []);

Fetch 中,有两个触发生命周期的方法:

一个是使用者通过 options 参数传入的回调函数,比如

const { loading, run } = useRequest(editUsername, {
  onBefore: (params) => {
    message.info(`Start Request: ${params[0]}`);
  },
  onSuccess: (result, params) => {
    message.success(`The username was changed to "${params[0]}" !`);
  },
  onError: (error) => {
    message.error(error.message);
  },
  onFinally: (params, result, error) => {
    message.info(`Request finish`);
  },
});

Fetch 中,在发送异步操作的各个节点执行回调

class Fetch<TData, TParams extends any[]> {

  async runAsync(...params: TParams): Promise<TData> {
    // ...

    // ✅ 执行 onBefore 回调
    this.options.onBefore?.(params);

    try {
      const res = await this.serviceRef.current(...params);

      // ✅ 执行 onSuccess 回调
      this.options.onSuccess?.(res, params);
      // ✅ 执行 onFinally 回调
      this.options.onFinally?.(params, res, undefined);
      
      return res;
    } catch (error) {
      // ✅ 执行 onError 回调
      this.options.onError?.(error, params);
      this.options.onFinally?.(params, undefined, error);

      throw error;
    }
  }
}

另一个则是插件方法执行后返回的事件订阅,下面是一个插件的实例代码

const useAutoRunPlugin: Plugin<any, any[]> = (
  fetchInstance,
  fetchOptions,
) => {
  useEffect(() => {
    // ...
  }, []);

  return {
    onBefore: () => {
      // ...
    },
    onRequest: () => {},
    onSuccess: () => {}
  };
};

Fetch 源码中,通过 runPluginHandler 方法来执行插件返回的执行 event,看源码:

class Fetch<TData, TParams extends any[]> {
  runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
    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;

    // ✅ 执行 onBefore 回调
    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);
    }

    try {
      // replace service
      // ✅ 执行 onRequest 回调
      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(() => {});
      }

      this.setState({
        data: res,
        error: undefined,
        loading: false,
      });

      // ✅ 执行 onSuccess 回调
      this.runPluginHandler('onSuccess', res, params);

      if (currentCount === this.count) {
        // ✅ 执行 onFinally 回调
        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,
      });

      // ✅ 执行 onError 回调
      this.runPluginHandler('onError', error, params);

      if (currentCount === this.count) {
        this.runPluginHandler('onFinally', params, undefined, error);
      }

      throw error;
    }
  }
}

我们可以看到,插件的生命周期事件不仅是获取信息,还可以给 Fetch 回传参数,来控制核心功能里的一些逻辑。

我们找一个插件看下具体逻辑,比如 useAutoRunPlugin.ts 这个插件实现的功能是:

通过设置 options.ready,可以控制请求是否发出。当其值为 false 时,请求永远都不会发出。

其具体行为如下:

  1. 当 manual=false 自动请求模式时,每次 ready 从 false 变为 true 时,都会自动发起请求,会带上参数 options.defaultParams
  2. 当 manual=true 手动请求模式时,只要 ready=false,则通过 run/runAsync 触发的请求都不会执行。
// useAutoRunPlugin.ts
import { useRef } from 'react';
import useUpdateEffect from '../../../useUpdateEffect';
import type { Plugin } from '../types';

const useAutoRunPlugin: Plugin<any, any[]> = (
  fetchInstance,
  // 使用者传入的 options
  { manual, ready = true, defaultParams = [] },
) => {

  // 插件内部可以使用 react hooks
  useUpdateEffect(() => {
    if (!manual && ready) {
      fetchInstance.run(...defaultParams);
    }
  }, [ready]);

  return {
    // 注册生命周期回调
    onBefore: () => {
      if (!ready) {
        return {
          stopNow: true,
        };
      }
    },
  };
};

useAutoRunPlugin.onInit = ({ ready = true, manual }) => {
  return {
    loading: !manual && ready,
  };
};

export default useAutoRunPlugin;

它有几个核心思路:

  1. 因为 useRequest 是一个自定义 hooks,因此这些插件也是自定义 hooks,可以使用 hooks 实现功能
  2. 依赖 ready 的变化,为 true 时,调用 fetchInstance.run 方法执行异步操作
  3. 通过在 onBefore 生命周期中返回 stopNow 来中断请求,我们看下 Fetch 的逻辑,在 runAsync 中获取到 stopNowtrue 时直接返回了空的 Promise
async runAsync(...params: TParams): Promise<TData> {
  const {
    stopNow = false,
    returnNow = false,
    ...state
  } = this.runPluginHandler('onBefore', params);

  // stop request
  if (stopNow) {
    return new Promise(() => {});
  }
}
  1. 通过 useAutoRunPlugin.onInit 方法设置 state.loading 对象

小结

其他的插件的实现思路大同小异,都是使用 React hooks 和生命周期函数来实现功能。

大家是否有发现,useRequest 的第三个入参是插件数组,外部可以传入自定义插件,但在使用文档中并没有写,我猜作者是不想让我们传入自定义插件,从源码上看,它的插件实现并没有实现核心与插件的完全解耦,比如 useAutoRunPlugin 返回的 stopNow 参数,在 Fetch 中直接使用了,如果 useAutoRunPlugin 有修改,那 Fetch 核心逻辑也要改动。总不能让大家传入自定义插件的同时,还要提个 PR 去改 Fetch吧,不过对于 useRequest 来说,这种简单的插件设计也够用了。

最后,通过阅读 useRequest 源码,我们对生命周期和插件式编程思想应该都有了入门级的理解,对今后实现自身业务也会有一定帮助。