useRequest - vue3版本

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

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

ts 复制代码
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的代码如下

js 复制代码
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是有返回值要求的。 例如:

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

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

js 复制代码
/**
   * 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 (插件作用数组)的某个作用返回值结果赋值到一个对象上(后面的作用结果会覆盖前面的)。最终结果长下面这样

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

2.请求状态

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

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

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

js 复制代码
  // 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

js 复制代码
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 这几个生命周期先执行"插件作用"

值得注意的几个点 :

  • let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.value, params); 说明onRequest作用是可能调用 service 产生promise的,如果插件没有调用,那么就在Fetch中调用。然后用requestReturnResponse 来处理这个promise.
  • this.countcurrentCount的作用: runAsync多次,只保留最后一次的结果

这里有个疑惑,如果多个插件调用了service产生promise,岂不是有的promise 创建了但浪费了? ------ 确实会有这种可能,但源码中给出了解决方法,参考useCachePlugin.

使用cachePromise 来保证只有一个service promise!

js 复制代码
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.首先看插件的使用,第三个参数传递 「插件数组」

js 复制代码
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个生命周期

  • onInit
  • onBefore
  • onRequest
  • onError
  • onSuccess
  • onFinally
  • onMutate

下面将分析内置的插件Plugin

useCachePlugin - 缓存

预备知识

首先介绍源码中的三个重要的工具

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

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

utis/cachePromise.ts

使用Map缓存Promise (当promise结束了删除对应缓存)

utils/cacheSubscribe.ts

订阅发布的实现。暴露3个API trigger, subscribe, otherSubscribe

插件的参数

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

js 复制代码
useCachePlugin(
  fetchInstance,
  {
    cacheKey,
    cacheTime = 5 * 60 * 1000,
    staleTime = 0,
    setCache: customSetCache,
    getCache: customGetCache,
  },
)
  • 如果定义了cacheKey,那么就会启动缓存
  • cacheTime和staleTime的区别:cacheTime是对缓存而言用来缓存数据的,先使用缓存数据,等请求新数据到了做替换;staleTime是为了用来避免短时间内重复请求的;
  • 如果定义了setCache和getCache,就使用自定义的缓存。(默认使用Map来缓存的)

缓存实现的思路

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

js 复制代码
// 数据是新鲜就停止请求
  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就是之后用来计算是否超过保鲜期使用的。

useRequesetImplement

问题探索

如何取消

如何缓存

如何刷新依赖

如何并行请求

如何节流防抖

相关推荐
P7Dreamer5 小时前
Vue 表格悬停复制指令:优雅地一键复制单元格内容
前端·vue.js
鹏多多5 小时前
Web图像编辑神器tui.image-editor从基础到进阶的实战指南
前端·javascript·vue.js
老华带你飞15 小时前
社区互助|基于SSM+vue的社区互助平台的设计与实现(源码+数据库+文档)
java·前端·数据库·vue.js·小程序·毕设·社区互助平台
chxii16 小时前
7.2elementplus的表单布局与模式
javascript·vue.js·elementui
dreams_dream17 小时前
vue中的与,或,非
前端·javascript·vue.js
1024小神18 小时前
vue/react项目如何跳转到一个已经写好的html页面
vue.js·react.js·html
古夕1 天前
Vue 3 复杂表单父子组件双向绑定的最佳实践
前端·javascript·vue.js
anyup1 天前
太全面啦!总结篇!99% 开发者可能都会遇到的 uView Pro 组件库问题
前端·vue.js·uni-app
lyq3151 天前
Vue2中extend 的作用
vue.js