解读 ahooks 源码系列 - Request 篇

发布于 2025-02-09 14:50:18 字数 36351 浏览 12 评论 0

本篇文章是解读 ahooks@3.8.0 源码系列的第一篇 - Request 篇,欢迎您的指正和点赞。

本文主要解读 useRequest 的入口、Fetch 类、useRequestImplement、Loading Delay、轮询、依赖刷新、屏幕聚焦重新请求、防抖、节流、缓存 & SWR、错误重试的源码实现。

基础用法

文档地址

详细代码

useRequest 的模块分为三大块:Core、Plugins、utils。

Plugins: useRequest 通过插件式组件代码,基本上每个功能点对应一个插件。通过插件化机制降低了每个功能之间的耦合度,也降低了其本身的复杂度。

Core: 整个 useRequest 的核心代码,处理了整个请求的生命周期。

utils: 工具方法。

useRequest 入口

先从入口文件开始,hooks/src/useRequest/src/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 || []),
    useAutoRunPlugin,
    useCachePlugin,
    useDebouncePlugin,
    useLoadingDelayPlugin,
    usePollingPlugin,
    useRefreshOnWindowFocusPlugin,
    useRetryPlugin,
    useThrottlePlugin,
  ] as Plugin<TData, TParams>[]);
}

export default useRequest;

类型定义,hooks/src/useRequest/src/types.ts

import type Fetch from "./Fetch";

export interface FetchState<TData, TParams extends any[]> {
  loading: boolean;
  params?: TParams;
  data?: TData;
  error?: Error;
}

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 Service<TData, TParams extends any[]> = (
  ...args: TParams
) => Promise<TData>;

export interface Options<TData, TParams> {
  manual?: boolean;

  onBefore?: (params: TParams) => void;
  onSuccess?: (data: TData, params: TParams) => void;
  onError?: (e: Error, params: TParams) => void;
  onFinally?: (params: TParams, data?: TData, e?: Error) => void;

  defaultParams?: TParams;

  // TODO: 待续
}

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

export interface Result<TData, TParams extends any[]> {
  loading: boolean;
  data?: TData;
  params: TParams | [];
  error?: Error;
  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"];
}

export type Subscribe = () => void;

useRequestImplement 方法

主要负责对 Fetch 类进行实例化。

import type { Options, Plugin, Result, Service } from "./types";
import isDev from "../../../../utils/isDev";
import useLatest from "@/hooks/useLatest";
import useUpdate from "@/hooks/useUpdate";
import useCreation from "@/hooks/useCreation";
import useUnmount from "@/hooks/useUnmount";
import useMemoizedFn from "@/hooks/useMemoizedFn";
import useMount from "@/hooks/useMount";
import Fetch from "./Fetch";

function useRequestImplement<TData, TParams extends any[]>(
  service: Service<TData, TParams>,
  options: Options<TData, TParams> = {},
  plugins: Plugin<TData, TParams>[] = []
) {
  const { manual = false, ...rest } = options;

  if (isDev) {
    if (options.defaultParams && !Array.isArray(options.defaultParams)) {
      console.warn(
        `expected defaultParams is array, got ${typeof options.defaultParams}`
      );
    }
  }

  const fetchOptions = {
    manual,
    ...rest,
  };

  // service 实例
  const serviceRef = useLatest(service);

  const update = useUpdate();

  // 保证请求实例不会发生变化
  const fetchInstance = useCreation(() => {
    // 执行 某个 plugin 的 onInit 方法,初始化状态值
    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(() => {
    // 默认 false,在初始化时自动执行 service
    if (!manual) {
      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>;
}

export default useRequestImplement;

Fetch 类

/* eslint-disable @typescript-eslint/no-parameter-properties */
import type { MutableRefObject } from "react";
import type {
  FetchState,
  Options,
  PluginReturn,
  Service,
  Subscribe,
} from "./types";
import { isFunction } from "../../../../utils";

/**
 * 插件化机制
 * 只负责完成整体流程的功能,额外的功能都交给插件去实现
 * 符合职责单一原则:一个 Plugin 只负责一件事,相互之间不相关,可维护性高、可测试性强
 * 符合深模块的软件设计理念。https://www.cnblogs.com/hhelibeb/p/10708951.html
 * */
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(
    // 请求示例 ref
    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 设为 true
      ...initState,
    };
  }

  // 更新状态函数
  setState(s: Partial<FetchState<TData, TParams>> = {}) {
    this.state = {
      ...this.state,
      ...s,
    };
    // 更新,通知 useRequestImplement 组件重新渲染,获取到最新状态值
    this.subscribe();
  }

  // 执行特定阶段的插件方法,包含了一个请求从开始到结束的生命周期(:Mutate 除外
  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);
  }

  // 执行请求的核心方法!!!
  // 如果设置了 options.manual = true,则 useRequest 不会默认执行,需要通过 run 或者 runAsync 来触发执行。
  // runAsync 是一个返回 Promise 的异步函数,如果使用 runAsync 来调用,则意味着你需要自己捕获异常。
  async runAsync(...params: TParams): Promise<TData> {
    // 计数器(锁),cancel 请求需要
    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
      loading: true,
      params,
      ...state,
    });

    // return now
    // 立即返回,与缓存策略有关
    if (returnNow) {
      return Promise.resolve(state.data);
    }

    // options 的 onBefore 回调
    this.options.onBefore?.(params);

    // 执行请求
    try {
      // replace service
      // 与缓存策略有关,如果有 cache 的 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,
      });

      // options 的 onSuccess 回调
      this.options.onSuccess?.(res, params);

      // plugin 的 Onsuccess 事件
      this.runPluginHandler("onSuccess", res, params);

      // options 的 onFinally 回调
      this.options.onFinally?.(params, res, undefined);

      if (currentCount === this.count) {
        // plugin 的 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,
      });

      // options 的 onError 回调
      this.options.onError?.(error, params);

      // plugin 的 onError 事件
      this.runPluginHandler("onError", error, params);

      // options 的 onFinally 回调
      this.options.onFinally?.(params, undefined, error);

      // plugin 的 onFinally 事件
      if (currentCount === this.count) {
        this.runPluginHandler("onFinally", params, undefined, error);
      }

      // 抛出异常
      throw error;
    }
  }

  // run 同步函数,其内部调用了 runAsync 方法
  run(...params: TParams) {
    this.runAsync(...params).catch((error) => {
      if (!this.options.onError) {
        console.error(error);
      }
    });
  }

  // 取消当前正在进行的请求
  cancel() {
    // 设置 this.count + 1,在 runAsync 的执行过程中,通过判断 currentCount !== this.count,达到取消请求的目的
    this.count += 1;
    this.setState({
      loading: false,
    });

    // 执行 plugin 的 onCancel
    this.runPluginHandler("onCancel");
  }

  // 使用上一次的 params,重新调用 run
  refresh() {
    // @ts-ignore
    this.run(...(this.state.params || []));
  }

  // 使用上一次的 params,重新调用 runAsync
  refreshAsync() {
    // @ts-ignore
    return this.runAsync(...(this.state.params || []));
  }

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

Loading Delay

文档地址

详细代码

import type { Plugin, Timeout } from "../types";
import { useRef } from "react";

const useLoadingDelayPlugin: Plugin<any, any[]> = (
  fetchInstance,
  { loadingDelay, ready }
) => {
  const timerRef = useRef<Timeout>();

  if (!loadingDelay) {
    return {};
  }

  // 清除定时器
  const cancelTimout = () => {
    if (timerRef.current) {
      clearTimeout(timerRef.current);
    }
  };

  return {
    // 请求前调用
    onBefore: () => {
      cancelTimout();

      // 通过 setTimeout 实现延迟 loading 变为 true 的时间
      if (!ready) {
        timerRef.current = setTimeout(() => {
          fetchInstance.setState({
            loading: true,
          });
        }, loadingDelay);
      }

      // 不管是手动还是非手动,先在请求前把 loading 置为 false
      return {
        loading: false,
      };
    },
    onFinally: () => {
      cancelTimout();
    },
    onCancel: () => {
      cancelTimout();
    },
  };
};
export default useLoadingDelayPlugin;

轮询

文档地址

详细代码

import type { Plugin, Timeout } from "../types";
import useUpdateEffect from "@/hooks/useUpdateEffect";
import { useRef } from "react";
import isDocumentVisible from "../utils/isDocumentVisible";
import subscribeReVisible from "../utils/subscribeReVisible";

const usePollingPlugin: Plugin<any, any[]> = (
  fetchInstance,
  { pollingInterval, pollingWhenHidden = true, pollingErrorRetryCount = -1 }
) => {
  const timerRef = useRef<Timeout>();
  // 清除订阅事件的函数
  const unsubscribeRef = useRef<() => void>();
  // 执行错误次数
  const countRef = useRef<number>(0);

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

    // 执行清除订阅事件的函数
    unsubscribeRef.current?.();
  };

  useUpdateEffect(() => {
    if (!pollingInterval) {
      stopPolling();
    }
  }, [pollingInterval]);

  // pollingInterval: 轮询间隔,单位为毫秒。如果值大于 0,则处于轮询模式,否则直接返回。
  if (!pollingInterval) {
    return {};
  }

  return {
    onBefore: () => {
      stopPolling();
    },
    onError: () => {
      countRef.current += 1;
    },
    onSuccess: () => {
      countRef.current = 0;
    },
    // 在 onFinally 阶段,通过定时器 setTimeout 进行轮询
    onFinally: () => {
      if (
        // pollingErrorRetryCount: 轮询错误重试次数。如果设置为 -1,则无限次
        pollingErrorRetryCount === -1 ||
        // When an error occurs, the request is not repeated after pollingErrorRetryCount retries
        (pollingErrorRetryCount !== -1 &&
          countRef.current <= pollingErrorRetryCount)
      ) {
        timerRef.current = setTimeout(() => {
          // pollingWhenHidden: 在页面隐藏时,是否继续轮询。如果设置为 false,在页面隐藏时会暂时停止轮询,页面重新显示时继续上次轮询
          // if pollingWhenHidden = false && document is hidden, then stop polling and subscribe revisible
          if (!pollingWhenHidden && !isDocumentVisible()) {
            // 通过 subscribeReVisible 进行订阅,并返回清除订阅事件的函数
            unsubscribeRef.current = subscribeReVisible(() => {
              fetchInstance.refresh();
            });
          } else {
            fetchInstance.refresh();
          }
        }, pollingInterval);
      } else {
        countRef.current = 0;
      }
    },
    onCancel: () => {
      stopPolling();
    },
  };
};

export default usePollingPlugin;

Ready

文档地址

依赖刷新

文档地址

详细代码

import useUpdateEffect from "@/hooks/useUpdateEffect";
import { useRef } from "react";
import type { Plugin } from "../types";

// support refreshDeps & ready
const useAutoRunPlugin: Plugin<any, any[]> = (
  fetchInstance,
  {
    manual,
    ready = true,
    defaultParams = [],
    refreshDeps = [],
    refreshDepsAction,
  }
) => {
  const hasAutoRun = useRef(false);
  hasAutoRun.current = false;

  /**
   * Ready
   * useUpdateEffect 等同于 useEffect,但会忽略首次执行,只在依赖更新时执行
   * */
  useUpdateEffect(() => {
    // 自动请求模式并且 ready 值为 true
    if (!manual && ready) {
      hasAutoRun.current = true;
      // 执行请求
      fetchInstance.run(...defaultParams);
    }
  }, [ready]);

  /**
   * 依赖刷新
   * */
  useUpdateEffect(() => {
    if (hasAutoRun.current) {
      return;
    }
    // 自动请求模式
    if (!manual) {
      hasAutoRun.current = true;
      // 自定义依赖数组变化时的请求行为
      if (refreshDepsAction) {
        refreshDepsAction();
      } else {
        // 调用 refresh 方法,实现刷新重复上一次请求的效果
        fetchInstance.refresh();
      }
    }
  }, [...refreshDeps]);

  return {
    // 在 onBefore 阶段,当 ready 值为 false 时,返回 stopNow
    onBefore: () => {
      if (!ready) {
        return {
          stopNow: true,
        };
      }
    },
  };
};

export default useAutoRunPlugin;

屏幕聚焦重新请求

文档地址

详细代码

// 使用闭包的简易版节流函数
export default function limit(fn: any, timespan: number) {
  // 设置一个标识位,标识还在 pending 阶段,不进行请求
  let pending = false;
  return (...args: any[]) => {
    // 正处于 pending,则直接返回
    if (pending) return;
    pending = true;
    fn(...args);
    setTimeout(() => {
      // 标识位置为 false,允许请求
      pending = false;
    }, timespan);
  };
}
import isBrowser from "../../../../../utils/isBrowser";

export default function isOnline(): boolean {
  if (isBrowser && typeof navigator.onLine !== "undefined") {
    return navigator.onLine;
  }
  return true;
}
import isBrowser from "../../../../../utils/isBrowser";

export default function isDocumentVisible(): boolean {
  if (isBrowser) {
    // document.visibilityState 只读属性,返回 document 的可见性
    return document.visibilityState !== "hidden";
  }

  return true;
}
import isBrowser from "../../../../../utils/isBrowser";
import isDocumentVisible from "../utils/isDocumentVisible";
import isOnline from "../utils/isOnline";

type Listener = () => void;

// 全局变量,维护一个事件队列,存放订阅的事件
const listeners: Listener[] = [];

// 订阅事件
function subscribe(listener: Listener) {
  listeners.push(listener);
  // 返回取消订阅函数
  return function unsubscribe() {
    const index = listeners.indexOf(listener);
    if (index > -1) {
      listeners.splice(index, 1);
    }
  };
}

if (isBrowser) {
  const revalidate = () => {
    // document 不可见,或者断网时,直接返回
    if (!isDocumentVisible() || !isOnline()) return;
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }
  };
  // 监听 visibilitychange 事件
  window.addEventListener("visibilityChange", revalidate, false);
  // 监听 focus 事件
  window.addEventListener("focus", revalidate, false);
}

export default subscribe;
import { useEffect, useRef } from "react";
import type { Plugin } from "../types";
import useUnmount from "@/hooks/useUnmount";
import limit from "../utils/limit";
import subscribeFocus from "../utils/subscribeFocus";

const useRefreshOnWindowFocusPlugin: Plugin<any, any[]> = (
  fetchInstance,
  { refreshOnWindowFocus, focusTimespan = 5000 }
) => {
  // 清除订阅事件的函数
  const unsubscribeRef = useRef<() => void>();

  const stopSubscribe = () => {
    // 执行清除订阅事件的函数
    unsubscribeRef.current?.();
  };

  /**
   * options.refreshOnWindowFocus、options.focusTimespan 支持动态变化
   * */
  useEffect(() => {
    // options.refreshOnWindowFocus 为 true,在屏幕重新获取焦点或重新显示时,重新发起请求
    // (: 默认自上一次请求后回到页面的时间间隔大于 5000ms,才重新发起请求
    if (refreshOnWindowFocus) {
      // 根据 focusTimespan,判断是否进行请求
      const limitRefresh = limit(
        fetchInstance.refresh.bind(fetchInstance),
        focusTimespan
      );
      // 存放在订阅事件列表中
      unsubscribeRef.current = subscribeFocus(() => {
        limitRefresh();
      });
    }
    return () => {
      stopSubscribe();
    };
  }, [refreshOnWindowFocus, focusTimespan]);

  useUnmount(() => {
    stopSubscribe();
  });

  return {};
};

export default useRefreshOnWindowFocusPlugin;

防抖

文档地址

详细代码

import type { Plugin } from "../types";
import debounce from "lodash/debounce";
import { useRef, useMemo, useEffect } from "react";
import type { DebouncedFunc, DebounceSettings } from "lodash";

/**
 * 函数劫持,加入防抖逻辑
 * loadash debounce: 创建一个 debounced(防抖动)函数,该函数会从上一次被调用后,延迟 wait 毫秒后调用 func 方法。
 * https://www.lodashjs.com/docs/lodash.debounce
 * */
const useDebouncePlugin: Plugin<any, any[]> = (
  fetchInstance,
  { debounceWait, debounceLeading, debounceTrailing, debounceMaxWait }
) => {
  const debouncedRef = useRef<DebouncedFunc<any>>();

  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]);

  useEffect(() => {
    if (debounceWait) {
      // 保存原函数
      const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);

      // 使用 lodash 的 debounce
      // 该函数提供一个 cancel 方法取消延迟的函数调用
      debouncedRef.current = debounce(
        (callback) => {
          callback();
        },
        debounceWait,
        options
      );

      // 函数劫持,改写 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);
          });
        });
      };

      return () => {
        // cancel 掉防抖函数
        debouncedRef.current?.cancel();
        // 还原原函数
        fetchInstance.runAsync = _originRunAsync;
      };
    }
  }, [debounceWait, options]);

  if (!debounceWait) {
    return {};
  }

  return {
    onCancel: () => {
      debouncedRef.current?.cancel();
    },
  };
};

export default useDebouncePlugin;

节流

文档地址

详细代码

import type { Plugin } from "../types";
import throttle from "lodash/throttle";
import { useRef, useMemo, useEffect } from "react";
import type { DebouncedFunc, ThrottleSettings } from "lodash";

/**
 * 函数劫持,加入节流逻辑
 * loadash throttle: 创建一个节流函数,在 wait 秒内最多执行 func 一次的函数。
 * https://www.lodashjs.com/docs/lodash.throttle
 * */
const useThrottlePlugin: Plugin<any, any[]> = (
  fetchInstance,
  { throttleWait, throttleLeading, throttleTrailing }
) => {
  const throttledRef = useRef<DebouncedFunc<any>>();

  const options = useMemo(() => {
    const ret: ThrottleSettings = {};
    if (throttleLeading !== undefined) {
      ret.leading = throttleLeading;
    }
    if (throttleTrailing !== undefined) {
      ret.trailing = throttleTrailing;
    }
    return ret;
  }, [throttleLeading, throttleTrailing]);

  useEffect(() => {
    if (throttleWait) {
      // 保存原函数
      const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);

      // 使用 lodash 的 throttle
      // 该函数提供一个 cancel 方法取消延迟的函数调用
      throttledRef.current = throttle(
        (callback) => {
          callback();
        },
        throttleWait,
        options
      );

      // 函数劫持,改写 runAsync 方法,使其具有节流能力
      // throttle runAsync should be promise
      // https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
      fetchInstance.runAsync = (...args) => {
        return new Promise((resolve, reject) => {
          throttledRef.current?.(() => {
            // 执行原函数
            _originRunAsync(...args)
              .then(resolve)
              .catch(reject);
          });
        });
      };

      return () => {
        // cancel 掉节流函数
        throttledRef.current?.cancel();
        // 还原原函数
        fetchInstance.runAsync = _originRunAsync;
      };
    }
  }, [throttleWait, options]);

  if (!throttleWait) {
    return {};
  }

  return {
    onCancel: () => {
      throttledRef.current?.cancel();
    },
  };
};

export default useThrottlePlugin;

缓存 & SWR

文档地址

详细代码

type Timer = ReturnType<typeof setTimeout>;
type CachedKey = string | number;

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>();

// 设置缓存
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,则表示缓存数据永不过期
  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 {
    // 如果 cacheKey 为空,则清空所有缓存数据
    cache.clear();
  }
};

export { getCache, setCache, clearCache };
type CachedKey = string | number;

const cachePromise = new Map<CachedKey, Promise<any>>();

const getCachePromise = (cacheKey: CachedKey) => {
  return cachePromise.get(cacheKey);
};

const setCachePromise = (cacheKey: CachedKey, promise: Promise<any>) => {
  // 应该缓存相同的 promise,不能是 promise.finally
  // Should cache the same promise, cannot be promise.finally
  // 因为 promise.finally 会改变 promise 的引用
  // Because the promise.finally will change the reference of the promise
  cachePromise.set(cacheKey, promise);

  // 监听 promise 状态,promise 成功或被拒绝,从缓存中删除对应的 cacheKey
  promise
    .then((res) => {
      // 在 then 和 cache 中都将 promise 缓存删除
      cachePromise.delete(cacheKey);
      return res;
    })
    .catch(() => {
      // 在 then 和 cache 中都将 promise 缓存删除
      cachePromise.delete(cacheKey);
    });
};

export { getCachePromise, setCachePromise };
type Listener = (data: any) => void;

// 全局变量,维护一个事件队列,存放订阅的事件
const listeners: Record<string, Listener[]> = {};

// 触发某个属性(cacheKey) 的所有事件
const trigger = (key: string, data: any) => {
  if (listeners[key]) {
    listeners[key].forEach((item) => item(data));
  }
};

// 订阅事件
const subscribe = (key: string, listener: Listener) => {
  // 每个属性(cacheKey) 维护一个事件列表
  if (!listeners[key]) {
    listeners[key] = [];
  }

  listeners[key].push(listener);

  // 返回清除订阅函数
  return function unsubscribe() {
    const index = listeners[key].indexOf(listener);
    listeners[key].splice(index, 1);
  };
};

export { trigger, subscribe };
import { useRef } from "react";
import type { Plugin } from "../types";
import { setCache, getCache } from "../utils/cache";
import type { CachedData } from "../utils/cache";
import useUnmount from "@/hooks/useUnmount";
import useCreation from "@/hooks/useCreation";
import { subscribe, trigger } from "../utils/cacheSubscribe";
import { getCachePromise, setCachePromise } from "../utils/cachePromise";

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

  const currentPromiseRef = useRef<Promise<any>>();

  // 缓存数据
  const _setCache = (key: string, cachedData: CachedData) => {
    // 有自定义设置缓存配置,优先使用自定义缓存
    if (customSetCache) {
      customSetCache(cachedData);
    } else {
      // 调用 cache utils 中的 setCache 函数
      setCache(key, cacheTime, cachedData);
    }
    // 触发 key 的所有事件。如果 key 相同,就可以共享缓存的数据
    trigger(key, cachedData.data);
  };

  const _getCache = (key: string, params: any[] = []) => {
    // 有自定义获取缓存配置,优先使用自定义缓存
    if (customGetCache) {
      return customGetCache(params);
    }
    // 调用 cache utils 中的 getCache 函数
    return getCache(key);
  };

  // 初始化逻辑
  useCreation(() => {
    if (!cacheKey) {
      return;
    }

    // get data from cache when init
    const cacheData = _getCache(cacheKey);
    if (cacheData && Object.hasOwnProperty.call(cacheData, "data")) {
      // 直接使用缓存中的 data 和 params 进行替代,返回结果
      fetchInstance.state.data = cacheData.data;
      fetchInstance.state.params = cacheData.params;
      // staleTime 为 -1 或还存在于新鲜时间内,则设置 loading 为 false
      if (
        staleTime === -1 ||
        new Date().getTime() - cacheData.time <= staleTime
      ) {
        fetchInstance.state.loading = false;
      }
    }

    // subscribe same cachekey update, trigger update
    // 订阅同一个 cacheKey 的更新。假如两个都是用的同一个 cacheKey,它们的内容可以全局同享
    unSubscribeRef.current = subscribe(cacheKey, (data) => {
      fetchInstance.setState({ data });
    });
  }, []);

  useUnmount(() => {
    unSubscribeRef.current?.();
  });

  if (!cacheKey) {
    return {};
  }

  return {
    // 请求前
    onBefore: (params) => {
      // 获取缓存
      const cacheData = _getCache(cacheKey, params);

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

      // staleTime 为 -1 或还存在于新鲜时间内,直接返回,不需要重新请求
      // 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,
        };
      }
    },
    // 请求阶段,缓存 promise。保证在同一时间点,采用了同一个 cacheKey 的请求只有一个请求被发起
    onRequest: (service, args) => {
      // 查看 promise 有没有缓存
      // 假如 promise 已经执行完成,则为 undefined。也就是没有同样 cacheKey 在执行。
      let servicePromise = getCachePromise(cacheKey);

      // If has servicePromise, and is not trigger by self, then use it
      // 如果有 servicePromise,并且不等于之前自己触发的请求,那么就使用它
      if (servicePromise && servicePromise !== currentPromiseRef.current) {
        return { servicePromise };
      }

      // 执行请求
      servicePromise = service(...args);
      // 保存本次触发的 promise 值
      currentPromiseRef.current = servicePromise;
      // 设置 promise 缓存
      setCachePromise(cacheKey, servicePromise);
      return { servicePromise };
    },
    // 请求成功
    onSuccess: (data, params) => {
      if (cacheKey) {
        // cancel subscribe, avoid trigger self
        // 取消订阅,避免触发到自己
        unSubscribeRef.current?.();
        // 缓存数据
        _setCache(cacheKey, {
          data,
          params,
          time: new Date().getTime(),
        });
        // resubscribe
        // 重新订阅以获取更新后的数据
        unSubscribeRef.current = subscribe(cacheKey, (d) => {
          fetchInstance.setState({ data: d });
        });
      }
    },
    // 手动修改 data
    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 });
        });
      }
    },
  };
};

export default useCachePlugin;

错误重试

文档地址

详细代码

import type { Plugin, Timeout } from "../types";
import { useRef } from "react";

const useRetryPlugin: Plugin<any, any[]> = (
  fetchInstance,
  { retryCount, retryInterval }
) => {
  const timerRef = useRef<Timeout>();
  // 记录 retry 的次数
  const countRef = useRef(0);

  // 标记是否由重试触发
  const triggerByRetry = useRef(false);

  if (!retryCount) {
    return {};
  }

  return {
    /**
     * 避免 useRequest 重新执行,导致请求重新发起
     * 在 onBefore 里做一些重置处理,以防和上一次的 retry 定时器撞车
     * */
    onBefore: () => {
      // 不是由重试触发,重置 count
      if (!triggerByRetry.current) {
        countRef.current = 0;
      }
      // 重置 triggerByRetry 为 false
      triggerByRetry.current = false;

      if (timerRef.current) {
        clearTimeout(timerRef.current);
      }
    },
    onSuccess: () => {
      // 重置为 0
      countRef.current = 0;
    },
    onError: () => {
      countRef.current += 1;
      // 重试的次数小于设置的次数
      // 或者 retryCount 设置为 -1,无限次重试
      if (retryCount === -1 || countRef.current <= retryCount) {
        // Exponential backoff
        // 如果不设置,默认采用简易的指数退避算法,取 1000 * 2 ** retryCount,也就是第一次重试等待 2s,第二次重试等待 4s,以此类推,如果大于 30s,则取 30s
        const timeout =
          retryInterval ?? Math.min(1000 * 2 ** countRef.current, 30000);
        timerRef.current = setTimeout(() => {
          // triggerByRetry 置为 true,保证重试次数不重置
          triggerByRetry.current = true;
          // 重新请求
          fetchInstance.refresh();
        }, timeout);
      } else {
        // 重置为 0
        countRef.current = 0;
      }
    },
    onCancel: () => {
      // 重置为 0
      countRef.current = 0;
      // 清除定时器
      if (timerRef.current) {
        clearTimeout(timerRef.current);
      }
    },
  };
};

export default useRetryPlugin;

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

神经大条

暂无简介

文章
评论
27 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文