您的位置: 首页> Vue> useRequest - vue3版本

useRequest - vue3版本

时间:2025-09-06 11:00:02 来源:互联网

受到ahooks的useRequest启发,vue-hooks-plus借鉴了ahooks的思想,并且大部分代码是可以复用。

下面就以vue-hooks-plus的useRequest来分析实现原理

const {
  loading: Readonly<Ref<boolean>>,
  data?: Readonly<Ref<TData>>,
  error?: Readonly<Ref<Error>>,
  params: Readonly<Ref<TParams | []>>,
  run: (...params: TParams) => void,
  runAsync: (...params: TParams) => Promise<TData>,
  refresh: () => void,
  refreshAsync: () => Promise<TData>,
  mutate: (data?: TData | ((oldData?: TData) => (TData | undefined))) => void,
  cancel: () => void,
} = useRequest<TData, TParams>(
  service: (...args: TParams) => Promise<TData>,
  {
    manual?: boolean,
    defaultParams?: TParams,
    formatResult?:(response:TData)=>unknown,
    onBefore?: (params: TParams) => void,
    onSuccess?: (data: TData, params: TParams) => void,
    onError?: (e: Error, params: TParams) => void,
    onFinally?: (params: TParams, data?: TData, e?: Error) => void,
  }
);

useRequest返回的这些方法,基本上都是挂载在Fetch实例上的,下面以Fetch为核心进行分析

核心 - Fetch

Fetch的代码如下

export default class Fetch<TData, TParams extends unknown[] = any> {

  // 插件配置
  pluginImpls: UseRequestPluginReturn<TData, TParams>[] | undefined;

  count = 0;

  // 内部请求状态
  state: UseRequestFetchState<TData, TParams> = {
    loading: false,
    params: undefined,
    data: undefined,
    error: undefined,
  };
  
  // 执行相关方法
  runAsync(){...};
  run(){...};
  refreshAsync(){...};
  refresh(){...};
  cancel(){...};
  mutate(){...};
}

Fetch被封装成一个class,我觉得可以分成三块来看:插件作用,请求状态,执行相关方法;
1.插件作用
对于插件,我们不妨先这样理解,插件是一个use函数,返回一个插件作用:
一个useXxxPlugin(插件)会返回一个 pluginImpls(插件作用), 上面??pluginImpls大概结果如下
pluginImpls : [{onBefore, onRequest, onSuccess, onError, onFinally}] onXx都是一个函数,onBefore和onRequest是有返回值要求的。 例如:

// onBefore的返回值
{
  stopNow?: boolean;
  returnNow?: boolean;
  loading?: boolean;
  params?: TParams;
  data?: TData;
  error?: Error | unknown;
}

然后定义了一个方法来执行插件作用数组的某一个“作用”,这些作用其实是和请求的生命周期对应的,即在请求的生命周期,执行对应的“作用”

/**
   * Traverse the plugin that needs to be run,
   * 插件的回调函数, 用于执行插件的逻辑.
   */
  runPluginHandler(event: keyof UseRequestPluginReturn<TData, TParams>, ...rest: unknown[]) {
    // @ts-ignore
    const r = (this.pluginImpls?.map(i => i[event]?.(...rest)) ?? [])?.filter(Boolean);
    // @ts-ignore
    return Object.assign({}, ...r);
  }

runPluginHandler 就是将pluginImpls (插件作用数组)的某个作用返回值结果赋值到一个对象上(后面的作用结果会覆盖前面的)。最终结果长下面这样

// runPluginHandler 返回结果
{
  stopNow?: boolean;
  returnNow?: boolean;
  loading?: boolean;
  params?: TParams;
  data?: TData;
  error?: Error | unknown;
}

2.请求状态
首先是内部定义了请求的状态

state: UseRequestFetchState<TData, TParams> = {
    loading: false,     //加载张图
    params: undefined,  //请求参数
    data: undefined,    //请求结果
    error: undefined,   //错误
  };

另外还设置了一个修改state的方法 setFetchState ,注意这个方法是需要调用外面的方法来修改外部的响应式变量。具体来说就是在useRequestImplement层:


  // reactive
  const state = reactive<UseRequestFetchState<TData, TParams>>({
    data: initialData,
    loading: false,
    params: undefined,
    error: undefined,
  });

  const setState = (currentState: unknown, field?: keyof typeof state) => {
    if (field) {
      state[field] = currentState as any;
    } else {
      if (isUseRequestFetchState<UnwrapRef<TData>, UnwrapRef<TParams>>(currentState)) {
        state.data = currentState.data;
        state.loading = currentState.loading;
        state.error = currentState.error;
        state.params = currentState.params;
      }
    }
  };

  const initState = plugins.map(p => p?.onInit?.(fetchOptions)).filter(Boolean);
  // Fetch Instance
  const fetchInstance = new Fetch<TData, TParams>(
    serviceRef,
    fetchOptions,
    setState,
    Object.assign({}, ...initState, state),
  );

在执行请求的生命周期,会使用setFetchState方法去修改请求状态。那么就引出了第3部分——执行相关方法。

3.执行相关方法

重点只需要关注runAsync方法,其他方法run/refreshAsync/refresh都是基于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,
    );
    // Do you want to stop the request
    if (stopNow) {
      return new Promise(() => { });
    }

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

    // Do you want to return immediately
    if (returnNow) {
      return Promise.resolve(state.data);
    }

    // The 'onBefore' configuration item error no longer interrupts the entire code flow
    try {
      // Return before request
      this.options.onBefore?.(params);
    } catch (error) {
      // The 'onBefore' configuration item error no longer interrupts the entire code flow
      this.setState({
        error,
        loading: false,
      });
      this.options.onError?.(error as Error, params);
      this.runPluginHandler('onError', error, params);

      // Manually intercept the error and return a Promise with an empty status
      return new Promise(() => { });
    }

    try {
      // Start the request with the replace service, if it contains the onRequest event name
      let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.value, params);

      const requestReturnResponse = (res: any) => {
        // The request has been cancelled, and the count will be inconsistent with the currentCount
        if (currentCount !== this.count) {
          return new Promise(() => { });
        }
        // Format data
        const formattedResult = this.options.formatResult ? this.options.formatResult(res) : res;

        this.setState({
          data: formattedResult,
          error: undefined,
          loading: false,
        });
        // Request successful
        this.options.onSuccess?.(formattedResult, params);

        this.runPluginHandler('onSuccess', formattedResult, params);

        this.previousValidData = formattedResult;

        // Execute whether the request is successful or unsuccessful
        this.options.onFinally?.(params, formattedResult, undefined);

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

        return formattedResult;
      };

      if (!servicePromise) {
        servicePromise = this.serviceRef.value(...params);
      }
      const servicePromiseResult = await servicePromise;
      return requestReturnResponse(servicePromiseResult);
    } catch (error) {
      if (currentCount !== this.count) {
        return new Promise(() => { });
      }

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

      this.options.onError?.(error as Error, params);
      this.runPluginHandler('onError', error, params);

      // rollback
      if (
        (isFunction(this.options?.rollbackOnError) && this.options?.rollbackOnError(params))
        || (isBoolean(this.options?.rollbackOnError) && this.options.rollbackOnError)
      ) {
        this.setState({
          data: this.previousValidData,
        });
      }

      // Execute whether the request is successful or unsuccessful
      this.options.onFinally?.(params, undefined, error as Error);

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

      throw error;
    }
  }

其实可以发现就是在 onBefore/onRequest/onSuccess/onError/onFinally 这几个生命周期先执行“插件作用”

值得注意的几个点 :

这里有个疑惑,如果多个插件调用了service产生promise,岂不是有的promise 创建了但浪费了? —— 确实会有这种可能,但实际写插件时需要你去判断是否已经存在promise.
可参考useCachePlugin的一个做法,使用cachePromise 来保证只有一个service promise

onRequest: (service, args) => {
      let servicePromise = cachePromise.getCachePromise(cacheKey)
      // 如果存在servicePromise,并且它没有被触发,则使用它
      if (servicePromise && servicePromise !== currentPromiseRef.value) {
        return { servicePromise }
      }

      servicePromise = service(...args)
      currentPromiseRef.value = servicePromise
      cachePromise.setCachePromise(cacheKey, servicePromise)
      return { servicePromise }
}

插件

1.首先看插件的使用,第三个参数传递 「插件数组」

const { data } = useRequest(
  () => serviceFn(),
  {
    ...option,
    pluginOptions: {
      ...pluginOption,
    },
  },
  [useFormatterPlugin, ...otherPlugins],
)

2.在useRequestImplement层:
plugins: [useFormatterPlugin, ...otherPlugins]
3.到Fetch层(执行useXxxPlugin后结果给到Fetch):
pluginImpls [{onBefore, onRequest, onSuccess, onError},{...}, ...]

插件是一个use函数,在useRequest里的流程,6个生命周期

下面将分析内置的插件Plugin

useCachePlugin - 缓存

基础原理

预备知识
首先介绍源码中的三个重要的工具

utils/cache.ts 使用Map缓存数据,但是有时间限制(使用setTimeout清理超时的数据)

setCache(
  key: CachedKey,
  cacheTime: number,  //缓存时间,单位毫秒
  cachedData: CachedData
)

utis/cachePromise.ts
使用Map缓存Promise (当promise结束了删除对应缓存)

utils/cacheSubscribe.ts
订阅发布的实现。暴露3个API trigger, subscribe, otherSubscribe

插件的参数

该插件的options(使用useRequest传递的option作为插件的option,其中定义了下面部分就会被当做useCachePlugin的options)定义体现在第二个参数上,如下:

useCachePlugin(
  fetchInstance,
  {
    cacheKey,
    cacheTime = 5 * 60 * 1000,
    staleTime = 0,
    setCache: customSetCache,
    getCache: customGetCache,
  },
)

缓存实现的思路
onBefore中获取缓存:
const cacheData = _getCache(cacheKey, params)
当获取的缓存数据在保鲜期staleTime内,那么就阻止后续流程,结束,直接返回数据。

// 数据是新鲜就停止请求
  if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
    return {
      loading: false,
      data: cacheData?.data,
      returnNow: true,
    }
  } else {
    // 数据不新鲜,则返回data,并且继续发送请求
    return {
      data: cacheData?.data,
    }
  }

onSuccess中缓存结果:

_setCache(cacheKey, {
  data,
  params,
  time: new Date().getTime(), //其中time就是之后用来计算是否超过保鲜期使用的。
})
如何缓存 - 共享数据

官方示例: useRequest缓存-共享数据

就是说useRequest被复用的情况下,某个定义有:
const {run} = useRequest(()=>{...}, {cacheKey:'a'})
然后组件在A、B中使用,A、B共用了cacheKey (service、fetchInstance这些其实都是独立分开的,仅使用了相同的cacheKey)

A组件run后更新数据会自动触发 B组件更新数据. A/B组件是“共享数据”,实现原理主要看下面的代码:

// useCachePlugin.ts 

watchEffect(() => {
    if (!cacheKey) {
      return;
    }

    // 获取初始化的data  (主要同步params参数)
    const cacheData = _getCache(cacheKey);
    if (cacheData && Object.hasOwnProperty.call(cacheData, 'data')) {
      fetchInstance.state.data = cacheData.data;
      fetchInstance.state.params = cacheData.params;
      if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
        fetchInstance.state.loading = false;
      }
    }

    // 如果存在相同的cacheKey,触发更新
    unSubscribeRef.value = cacheSubscribe.subscribe(cacheKey, (data) => {
      fetchInstance.setState({ data });
    });
  });

当插件被执行时,执行watchEffect,会subscribe订阅, 然后再请求结果success后会trigger触发 fetchInstance的状态!

// useCachePlugin.ts 

const _setCache = (key: string, cachedData: CachedData) => {
    if (customSetCache) {
      customSetCache(cachedData);
    } else {
      cache.setCache(key, cacheTime, cachedData);
    }
    cacheSubscribe.trigger(key, cachedData.data);
};

插件的执行发生在 useRequestImplement 内部

// useRequestImplement.ts

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

  const readyComputed = computed(() => (isRef(ready) ? ready.value : ready));

  watchEffect(() => {
    if (!manual) {
      const params = fetchInstance.state.params || options.defaultParams || [];
      // auto collect
      if (readyComputed.value && fetchInstance.options.refreshDeps === true && !!serviceRef.value) {
        fetchInstance.run(...(params as TParams));
      }
    }
  });

useCachePlugin 共享了promise,一个cacheKey只会发生一个promise

// useCachePlugin.ts 

onRequest: (service, args) => {
      let servicePromise = cachePromise.getCachePromise(cacheKey)
      // 如果存在servicePromise,并且它没有被触发,则使用它
      if (servicePromise && servicePromise !== currentPromiseRef.value) {
        return { servicePromise }
      }

      servicePromise = service(...args)
      currentPromiseRef.value = servicePromise
      cachePromise.setCachePromise(cacheKey, servicePromise)
      return { servicePromise }
}

useAutoRunPlugin - 自动插件

基础原理

插件的参数

useAutoRunPlugin(
  fetchInstance,
  { manual, ready = true, refreshDeps = [], refreshDepsAction },
)

主要监听 ready、refreshDeps来判断是否要刷新数据。其中manual决定是否第一次自动发请求是在useRequestImplement中实现了。

插件的实现

依赖刷新

参考官方案例 自动收集依赖

// useRequestImplement.ts

watchEffect(() => {
    if (!manual) {
      const params = fetchInstance.state.params || options.defaultParams || [];
      // auto collect
      if (readyComputed.value && fetchInstance.options.refreshDeps === true && !!serviceRef.value) {
        fetchInstance.run(...(params as TParams));
      }
    }
  });

  // manual
  if (!manual && fetchInstance.options.refreshDeps !== true) {
    const params = fetchInstance.state.params || options.defaultParams || [];
    if (unref(ready)) fetchInstance.run(...(params as TParams));
  }

这两段代码都是处理同一问题,当manual=false时自动触发请求,区别在于refreshDeps

注意,对于手动监听依赖,其实还需要做监听依赖变化刷新请求

// useAutoRunPlugin.ts 

  if (refreshDeps instanceof Array) watch(
    [hasAutoRun, ...refreshDeps],
    ([autoRun]) => {
      if (!autoRun) return;
      if (!manual && autoRun) {
        if (refreshDepsAction) {
          refreshDepsAction();
        } else {
          fetchInstance.refresh();
        }
      }
    },
    {
      deep: true,
      immediate: false,
    },
  );
  else watch(hasAutoRun, (h) => {
    if (!manual && h) {
      if (refreshDepsAction) {
        refreshDepsAction();
      } else {
        fetchInstance.refresh();
      }
    }
  });

useRequesetImplement

如何取消请求的

上一篇:vue3.5.18源码:一文搞懂响应式底层实现原理 下一篇:Vue.js 父子组件通信的十种方式

相关文章

相关应用

最近更新