ahooks useRequest使用与源码解析

前言

ahooks.js.org

useRequest 是 ahooks 中提供的用于异步数据管理的 Hook

useRequest 通过插件式组织代码,核心代码极其简单,并且可以很方便的扩展出更高级的功能。目前已有能力包括:

  • 自动请求/手动请求
  • 轮询
  • 防抖
  • 节流
  • 屏幕聚焦重新请求
  • 错误重试
  • loading delay
  • SWR(stale-while-revalidate)
  • 缓存

基本用法

默认请求

默认情况下,useRequest 第一个参数是一个异步函数,只需要返回一个 promise 即可。在组件初始化时,会自动执行该异步函数。同时自动管理该异步函数的 loading , data , error 等状态。

go 复制代码
const { data, error, loading } = useRequest(service);

手动触发

如果设置了 options.manual = true,则 useRequest 不会默认执行,需要通过 run 或者 runAsync 来触发执行。

arduino 复制代码
const { loading, run, runAsync } = useRequest(service, {
    manual: true
});


<button onClick={run} disabled={loading}>
    {loading ? 'Loading' : 'Edit'}
</button>

runrunAsync 的区别在于:

  • run 是一个普通的同步函数,自动捕获异常,可以通过 options.onError 来处理异常时的行为。

  • runAsync 是一个返回 Promise 的异步函数,如果使用 runAsync 来调用,则意味着需要手动捕获异常。

生命周期

useRequest 提供了以下几个生命周期配置项,以便在异步函数的不同阶段做一些处理。

  • onBefore:请求之前触发
  • onSuccess:请求成功触发
  • onError:请求失败触发
  • onFinally:请求完成触发
javascript 复制代码
const { loading, run } = useRequest(service, {
    manual: true,
    onBefore: (params) => {
        console.log(params);
    },
    onSuccess: (result, params) => {
        console.log(result, params);
    },
    onError: (error) => { },
    onFinally: (params, result, error) => { },
});

刷新(重复上一次请求)

useRequest 提供了 refreshrefreshAsync 方法,可以使用上一次的参数,重新发起请求。

typescript 复制代码
const { data, loading, run, refresh } = useRequest((id: number) => getUsername(id), {
    manual: true,
});

refreshrefreshAsync 的区别和 runrunAsync 一样。

立即变更数据

useRequest 提供了 mutate, 支持立即修改 useRequest 返回的 data 参数。

支持 mutate(newData)mutate((oldData) => newData) 两种写法。

mutate 的应用场景:

不希望等接口调用成功之后,才给用户反馈。而是直接修改页面数据,同时在背后去调用修改接口,等修改接口返回之后,另外提供反馈。

取消响应

useRequest 提供了 cancel 函数,用于忽略当前 promise 返回的数据和错误

需要注意的是:调用 cancel 函数并不会取消 promise 的执行

同时 useRequest 会在以下时机自动忽略响应:

  • 组件卸载时,忽略正在进行的 promise
  • 竞态取消,当上一次 promise 还没返回时,又发起了下一次 promise,则会忽略上一次 promise 的响应

参数管理

useRequest 返回的 params 会记录当次调用 service 的参数数组。如触发了 run(1, 2, 3),则 params 等于 [1, 2, 3]

如果设置了 options.manual = false,则首次调用 service 的参数可以通过 options.defaultParams 来设置。

轮询

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

arduino 复制代码
const { data, run, cancel } = useRequest(getUsername, {
  pollingInterval: 3000,
});

通过 cancel 来停止轮询,通过 run/runAsync 来启动轮询。

依赖刷新

useRequest 提供了一个 options.refreshDeps 参数,当它的值变化后,会重新触发请求。

scss 复制代码
const [userId, setUserId] = useState('1');

const { data, run } = useRequest(() => getUserSchool(userId), {
  refreshDeps: [userId],
});

错误重试

通过设置 options.retryCount,指定错误重试次数,则 useRequest 在失败后会进行重试。

arduino 复制代码
const { data, run } = useRequest(getUsername, {
  retryCount: 3,
});

缓存 & SWR

如果设置了 options.cacheKeyuseRequest 会将当前请求成功的数据缓存起来。下次组件初始化时,如果有缓存数据,会优先返回缓存数据,然后在背后发送新请求,也就是 SWR 。

通过 options.staleTime 设置数据保持新鲜时间,在该时间内,则认为数据是新鲜的,不会重新发起请求。

通过 options.cacheTime 设置数据缓存时间,超过该时间,会清空该条缓存数据。

php 复制代码
const { data, loading } = useRequest(getArticle, {
    cacheKey: 'staleTime-demo',
    staleTime: 5000,
  });

需要注意的是,同一个 cacheKey 的内容,在全局是共享的,这会带来以下几个特性

  • 请求 Promise 共享,相同的 cacheKey 同时只会有一个在发起请求,后发起的会共用同一个请求 Promise
  • 数据同步,任何时候,当改变其中某个 cacheKey 的内容时,其它相同 cacheKey 的内容均会同步

ahooks 提供了一个 clearCache 方法,通过 clearCache 方法,可以清除指定 cacheKey 的缓存数据。

源码解析

正如前文所述,useRequest 源码非常简单。下面来看一下。

入口

swift 复制代码
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>[]);
}

可以看到,useRequest 会将第三个参数作为 Plugin 传入。下面来看一下 Implement

useRequestImplement

scss 复制代码
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>;
}

大体逻辑如下:

  1. 创建请求实例 fetchInstance,后续如果需要再请求,则通过实例运行,同时通过 fetchInstance 管理请求参数以及请求状态。创建 fetchInstance 时会传入 fetchOptions 执行插件的 onInit 方法,并将 onInit 方法的返回值保存到 fetchInstance 上。
  2. 传入 fetchInstance 以及 fetchOptions 执行传入的 plugin,保存至 fetchInstancepluginImpls
  3. 如果没有设置 manualtrue,则执行请求。

核心 Fetch Class

可以看到, useRequest 的核心就是 fetchInstance。 通过 fetchInstance 管理了请求参数,请求状态,竞态条件等等。下面一起来看一下 Fetch

kotlin 复制代码
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[]) {
    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(() => {});
      }

      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() {
    this.run(...(this.state.params || []));
  }

  refreshAsync() {
    return this.runAsync(...(this.state.params || []));
  }

  mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
    const targetData = isFunction(data) ? data(this.state.data) : data;
    this.runPluginHandler('onMutate', targetData);
    this.setState({
      data: targetData,
    });
  }

可以看到,逻辑非常简单

在 ts 中,constructor 的入参中,带有可见性修饰符的参数会自动挂载到实例上。

Fetch 的核心就是 runPluginHandlerrunAsync 方法。

runPluginHandler 负责执行插件中特定的生命周期。

runAsync 负责请求,每一次执行 runAsync 方法,都会将 this.count + 1,并将当前的id保存下来,这个 count 其实就是请求的id。在后续的处理中,如果不是需要的id,则通过一个 pendingpromise 忽略返回结果。同时会在特定的时机执行特定的生命周期函数(包括 useRequest 入参的生命周期和插件返回的生命周期。

插件

useRequest 的插件是一个函数,入参为 fetchInstanceuseRequestoptions,返回一个生命周期对象,指定的函数会在请求对应的时机执行。

插件的 onInit 方法可以在初始化的时候修改 fetchInstance.state

typescript 复制代码
export 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;
}

export type Plugin<TData, TParams extends any[]> = {
  (fetchInstance: Fetch<TData, TParams>, options: Options<TData, TParams>): PluginReturn<
    TData,
    TParams
  >;
  onInit?: (options: Options<TData, TParams>) => Partial<FetchState<TData, TParams>>;
};

useRequest 提供的各种功能都是通过插件实现的。

下面来看一下缓存 & (SWR)的实现

缓存 & (SWR)

swr的实现是通过 useCachePlugin 实现的。 useCachePlugin 的核心方法如下。

ini 复制代码
const cache = new Map<CachedKey, RecordData>();

const setCache = (key: CachedKey, cacheTime: number, cachedData: CachedData) => {
  const currentCache = cache.get(key);
  if (currentCache?.timer) {
    clearTimeout(currentCache.timer);
  }

  let timer: Timer | undefined = undefined;

  if (cacheTime > -1) {
    // if cache out, clear it
    timer = setTimeout(() => {
      cache.delete(key);
    }, cacheTime);
  }

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

const getCache = (key: CachedKey) => {
  return cache.get(key);
};

const clearCache = (key?: string | string[]) => {
  if (key) {
    const cacheKeys = Array.isArray(key) ? key : [key];
    cacheKeys.forEach((cacheKey) => cache.delete(cacheKey));
  } else {
    cache.clear();
  }
};

通过一个共享 Map 对象保存请求的数据,缓存的时间,在后续发起请求时,如果数据有效,返回并终止请求,如果数据无效,返回缓存的数据并请求新的数据。同时由于是使用 Map 缓存,这意味着只要能保持引用稳定,可以使用任意 js 对象作为 key。核心逻辑如下:

javascript 复制代码
  return {
    onBefore: (params) => {
      const cacheData = _getCache(cacheKey, params);

      if (!cacheData || !Object.hasOwnProperty.call(cacheData, 'data')) {
        return {};
      }

      // If the data is fresh, stop request
      if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
        return {
          loading: false,
          data: cacheData?.data,
          error: undefined,
          returnNow: true,
        };
      } else {
        // If the data is stale, return data, and request continue
        return {
          data: cacheData?.data,
          error: undefined,
        };
      }
    },
    onRequest: (service, args) => {
      let servicePromise = getCachePromise(cacheKey);

      // If has servicePromise, and is not trigger by self, then use it
      if (servicePromise && servicePromise !== currentPromiseRef.current) {
        return { servicePromise };
      }

      servicePromise = service(...args);
      currentPromiseRef.current = servicePromise;
      setCachePromise(cacheKey, servicePromise);
      return { servicePromise };
    },
    onSuccess: (data, params) => {
      if (cacheKey) {
        // cancel subscribe, avoid trgger self
        unSubscribeRef.current?.();
        _setCache(cacheKey, {
          data,
          params,
          time: new Date().getTime(),
        });
        // resubscribe
        unSubscribeRef.current = subscribe(cacheKey, (d) => {
          fetchInstance.setState({ data: d });
        });
      }
    },
    onMutate: (data) => {
      if (cacheKey) {
        // cancel subscribe, avoid trigger self
        unSubscribeRef.current?.();
        _setCache(cacheKey, {
          data,
          params: fetchInstance.state.params,
          time: new Date().getTime(),
        });
        // resubscribe
        unSubscribeRef.current = subscribe(cacheKey, (d) => {
          fetchInstance.setState({ data: d });
        });
      }
    },
  };

其他的插件也很简单,比如 防抖节流 就是通过 lodash 对请求进行处理。

总结

学习了 useRequest 的使用及实现方式。 useRequest 通过插件式组织代码,核心代码极其简单,并且可以很方便的扩展出更高级的功能,而且提供了自定义的方法,如果已有的功能不能满足需求,只需要一个插件即可。

相关推荐
风清扬_jd33 分钟前
Chromium 硬件加速开关c++
java·前端·c++
谢尔登1 小时前
【React】事件机制
前端·javascript·react.js
2401_857622662 小时前
SpringBoot精华:打造高效美容院管理系统
java·前端·spring boot
etsuyou2 小时前
Koa学习
服务器·前端·学习
Easonmax2 小时前
【CSS3】css开篇基础(1)
前端·css
粥里有勺糖3 小时前
视野修炼-技术周刊第104期 | 下一代 JavaScript 工具链
前端·javascript·github
大鱼前端3 小时前
未来前端发展方向:深度探索与技术前瞻
前端
昨天;明天。今天。3 小时前
案例-博客页面简单实现
前端·javascript·css
天上掉下来个程小白3 小时前
请求响应-08.响应-案例
java·服务器·前端·springboot
前端络绎3 小时前
初识 DT-SDK:基于 Cesium 的二三维一体 WebGis 框架
前端