[ahooks] useRequest源码阅读(一)

前言

终于到useRequest了。useRequest是一个用于发起和管理业务组件中的网络请求,提供了网络请求常用的state(如loading, data)管理,轮询,防抖,节流,错误重试,SWR等功能。 可以说是ahooks库最核心的hook了

架构

useRequest采用插件化架构,大体可以拆分成核心模块插件系统两个部分:

核心模块通过一个Fetch类实现,主要用于发起请求,管理请求过程中产生的response,loading,error等数据。同时提供用于控制请求的方法: run, runAsync, cancel, refresh, mutate等。核心模块还提供了请求各个阶段的生命周期配置项

插件系统是在核心模块的基础上扩展了hooks功能,例如轮询,防抖,节流,缓存等都是由插件提供,这样设计的目的在于解耦和易于扩展

源码目录

我们先来看useRequest的源码目录:

  • plugins: 用于存储各个扩展功能的插件
  • utils: 工具函数
  • Fetch: 核心模块
  • types: 类型声明文件
  • useRequest: 调用useReuquestImplement传入参数
  • useReueqstImplement: 参数处理,生成Fetch实例,执行plugins等
  • index: 入口文件

插件

前面说到useRequest依赖它的插件机制来扩展功能,后续在分析源码时也绕不开这个机制,所以我们先来看它插件的类型声明:

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

可以知道它的插件本质上是一个函数,接收两个参数:

  • fetchInstance: 核心类的实例
  • options: 调用hooks时传入的配置对象

返回值是一个PluginReturn类型:

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

可以看到就是一个存储了插件各种生命周期函数的对象,与hooks的生命周期函数对比,插件的生命周期函数可以用于内部控制请求流程,甚至重写请求方法,功能更强。

除了入参数和返回值,我们还可以看到函数本身挂载了一个onInit方法,主要用于初始化状态

核心模块源码解析

核心模块是一个名为Fetch的范型类,我们先来看这两个范型:

typescript 复制代码
export default class Fetch<TData, TParams extends any[]> {}
  • TData: 异步请求函数的返回值
  • TParams: 异步请求函数需要的参数,这里extends any[]是将TParams约束成数组类型

这两个范型对应的参数会贯穿hooks整个请求流程,我们后面再讲

属性

接下来是这个类的属性:

typescript 复制代码
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: RefObject<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,
    };
  }
}
  • pluginImpls: PluginReturn数组,存储着所有插件的生命周期
  • count: 用于请求计数,防止出现竞态条件时,拿到旧的请求数据
  • state: 存储请求各个阶段的state
  • serviceRef: 调用hooks传入的请求函数
  • options: 调用hooks传入的配置项
  • subscribe: 用于触发React组件更新的函数
  • initState: 存储plugin onInit函数执行后的返回值,这里取最终结果

construction函数对state属性进行了初始化,可以看到initState里的参数会覆盖前面的

方法

ts 复制代码
export default class Fetch<TData, TParams extends any[]> {
  setState(s: Partial<FetchState<TData, TParams>> = {}) {}

runPluginHandler(event: keyof PluginReturn\<TData, TParams>, ...rest: any\[]){}

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

run(...params: TParams) {}

cancel() {}

refresh() {}

refreshAsync() {}

mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {}
}
  • setState: 用于更新state
  • runPluginHandler: 用于执行插件的各个生命周期
  • runAsync: 核心方法,包括发起请求,处理参数,调用插件和hook的生命周期等功能
  • run: runAsync的同步版本
  • cancel: 取消请求
  • refresh: 用于重新发送请求,里面调用run方法,传入上一次请求的参数
  • refreshAsync: 重新发送请求的异步版本,里面调用runAsync方法
  • mutate: 直接修改data的方法

runAsync

我们先来看最核心的runAsync方法,先是整体源码,下面是分步解析:

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

      // const formattedResult = this.options.formatResultRef.current ? this.options.formatResultRef.current(res) : res;

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

首先是将count属性递增1,然后创建一个currentCount变量存储当前的count值,这个变量刚刚说了是为了解决出现竞态场景时返回的数据不一致的问题,具体用法我们接着往下看:

typescript 复制代码
  this.count += 1;
  const currentCount = this.count;

接下来是调用plugin onBefore生命周期方法,返回两个关键属性stopNowreturnNow,前者用于终止请求,返回一个pending Promise;后者用于获取缓存的数据,返回一个resolve Promise:

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

发起请求

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

}

在发起请求前,先调用了hook的onBefore生命周期函数

然后调用插件的onRequest生命周期函数,返回一个请求后的Promise

可以看到这里做了一个判断,优先使用插件返回的Promise,如果没有,再调用传入的方法获取请求后的Promise:

typescript 复制代码
 // replace service
 let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);

  if (!servicePromise) {
    servicePromise = this.serviceRef.current(...params);
  }

  const res = await servicePromise;

接下来就是判断count值是否相等,若不相等,则返回一个pending Promise:

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

调用setState将保存返回的data

调用hook的onSuccess函数,插件的onSuccess函数,和hook的onFinally函数

这里还有一次count相等的判断,若相等,则再调用插件的onFinally函数

最后返回res:

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

错误处理

最后我们来看下runAsync的错误处理

首先仍然是count状态的检查,如果不相等则返回一个pending Promise,终止下面等操作

然后是存储error,将loading设置为false

分别调用hooks的onError,插件的onError,hooks的onFinally和插件的onFinally生命周期

最后抛出异常

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

小结

runAsyncFetch类的核心方法,内部实现了整个异步请求的生命周期管理,并通过count的设计巧妙实现了竞态条件的控制

hooks的生命周期:

  • onBefore
  • onSuccess
  • onError
  • onFinally

插件的生命周期:

  • onBefore
  • onRequest
  • onSuccess
  • onError
  • onFinally
  • onCancel
  • onMutate

下面我们来看其它几个方法的源码

setState

用于设置state,并调用subscribe方法触发组件的更新:

typescript 复制代码
setState(s: Partial<FetchState<TData, TParams>> = {}) {
  this.state = {
    ...this.state,
    ...s,
  };
  this.subscribe();
}

runPluginHandler

执行插件的各个生命周期,第一个参数用于指定生命周期类型,其余的是剩余参数:

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

run

runAsync的同步版本,可以看到直接调用runAsync方法,但没有返回值:

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

cancel

用于取消请求,这里可以看到它是通过改变count的值来实现请求的取消,并不是真的将http请求给取消了:

typescript 复制代码
cancel() {
  this.count += 1;
  this.setState({
    loading: false,
  });

  this.runPluginHandler('onCancel');
}

refresh

重新请求的方法:

typescript 复制代码
refresh() {
  // @ts-ignore
  this.run(...(this.state.params || []));
}

refreshAsync

重新请求方法的异步版本:

typescript 复制代码
refreshAsync() {
  // @ts-ignore
  return this.runAsync(...(this.state.params || []));
}

mutate

直接改变data的值:

typescript 复制代码
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 复制代码
/* eslint-disable @typescript-eslint/no-parameter-properties */
import type { RefObject } from 'react';
import { isFunction } from '../../utils';
import type { FetchState, Options, PluginReturn, Service, Subscribe } from './types';

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: RefObject<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[]) {
    // @ts-ignore
    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(() => {});
      }

      // const formattedResult = this.options.formatResultRef.current ? this.options.formatResultRef.current(res) : res;

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

  refreshAsync() {
    // @ts-ignore
    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,
    });
  }
}
相关推荐
小小愿望6 分钟前
移动端浏览器中设置 100vh 却出现滚动条?
前端·javascript·css
fail_to_code6 分钟前
请不要再只会回答宏任务和微任务了
前端
摸着石头过河的石头6 分钟前
taro3.x-4.x路由拦截如何破?
前端·taro
lpfasd12315 分钟前
开发Chrome/Edge插件基本流程
前端·chrome·edge
练习前端两年半1 小时前
🚀 Vue3 源码深度解析:Diff算法的五步优化策略与最长递增子序列的巧妙应用
前端·vue.js
烛阴1 小时前
TypeScript 接口入门:定义代码的契约与形态
前端·javascript·typescript
掘金安东尼1 小时前
使用自定义高亮API增强用户‘/’体验
前端·javascript·github
参宿72 小时前
electron之win/mac通知免打扰
java·前端·electron
石小石Orz2 小时前
性能提升60%:前端性能优化终极指南
前端·性能优化
夏日不想说话2 小时前
API请求乱序?深入解析 JS 竞态问题
前端·javascript·面试