ahooks源码之useRequest

前言

我们日常开发 React 项目时,不可避免的要发送很多请求,那如何提炼出一个满足大部分场景,并且又能够优雅使用的 Hooks 来提高我们开发的效率呢,接下来我们来通过源码看一下目前应用很广泛的 ahooks 的 useRequest 是如何做的。

useRequest

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 实际上返回了一个 useRequestImplement 的执行结果 这里比较特殊的是第三个参数,它是一个插件数组。

useRequest 提供了 Loading Delay、轮询、Ready 等等一系列功能,那这些功能其实都对应着一个插件,这样通过插件的机制很大程度上降低了各个功能之间的耦合度。

另外我们需要了解下这些插件执行后返回了什么,这个后面会用到。

const useCachePlugin: Plugin<any, any[]> = (/*...*/) => {
    // ...
    return {
        onBefore: (params) => { /*...*/ },
        onRequest: (service, args) => { /*...*/ },
        onSuccess: (data, params) => { /*...*/ },
        onMutate: (data) => { /*...*/ },
    };
};

这里用 useCachePlugin 插件做例子,可以看到他的返回值是一些回调钩子,其实他的意思就是在对应的钩子执行对应的方法。

接下来我们看下 useRequestImplement 是如何实现的。

useRequestImplement

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(() => {
        // ...
    }, []);
    fetchInstance.options = fetchOptions;
    fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
    useMount(() => {
        // ...
    });
    useUnmount(() => {
        //...
    });
    return {
        loading,
        data,
        error,
        params,
        cancel,
        refresh,
        refreshAsync,
        run,
        runAsync,
        mutate,
    };
}
  1. 首先看参数
    • service 是真正要请求的方法,
    • options 是配置项
    • plugins 是插件数组
  2. 再看内部定义的一些变量和方法
    • manual 是作为是否默认执行 useRequest 的依据,为 true 时,不会默认执行,需要手动触发
    • fetchOptions 是将配置项整合
    • serviceRef 返回当前最新的 service
    • update 可以理解为 forceUpdate, 让组件重新渲染
    • 这里最重要的就是 fetchInstance ,返回的所有值都要基于 fetchInstance 。

useMount / useUnmount

// 组件初始化执行
useMount(() => {
    if (!manual) {
        const params = fetchInstance.state.params || options.defaultParams || [];
        fetchInstance.run(...params);
    }
});
// 组件卸载执行
useUnmount(() => {
    fetchInstance.cancel();
});

这里看到hooks的名字应该也能猜到,这是两个生命周期,在组件初始化时,如果设置了 manual 为 true,就执行请求, 在组件销毁时,取消请求

fetchInstance

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;
// 执行所有的插件,将结果返回给 pluginImpls 属性
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));

fetchInstance 是一个通过 useCreation 创建出来的一个常量对象,这个对象是通过 new Fetch 创建的。然后将配置项和插件的执行结果赋值给对象的属性。fetchInstance 的一系列属性方法都是来自这个 Fetch。

Fetch

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>> = {}) {
        // ...
    }
    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)) {
        // ...
    }
}

首先在创建实例时,传入了四个参数,从 fetchInstance 中创建实例的时候可以看到这四个值分别对应什么,

  • serviceRef 就是要请求的 service
  • options 是配置项
  • subscribe 是让组件重新渲染
  • initState 是初始化的 state 值,

然后在 constructor 中初始化了 state。 剩下的除了 runPluginHandler 是内部使用的方法,其他都是在 useRequestImplement 抛出提供给我们使用的方法。 最重要的两个方法应该是 runPluginHandler 和 runAsync

setState

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

这个方法没什么好说的,就是更新 state,然后触发重新渲染。

runPluginHandler

runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
    // 执行存在 event 生命周期的插件,返回成功回调的结果,进行浅拷贝并返回
    const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);
    return Object.assign({}, ...r);
}

我们在开始的时候有介绍给插件的返回值是什么,runPluginHandler 就是在特定的阶段执行对应的插件方法,event 就是代表要执行的阶段。 我们来看一下 PluginReturn 类型,了解一下到底有多少个阶段。

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

runAsync

runAsync 就是真正去执行请求的方法,接下来我们根据不同的阶段来分别说明是如何处理的

onBefore

const {
    stopNow = false,
    returnNow = false,
    ...state
} = this.runPluginHandler('onBefore', params);

if (stopNow) {
    return new Promise(() => {
    });
}
// 更新 state
this.setState({
    loading: true,
    params,
    ...state,
});

// 如果开启缓存并且缓存没过期直接返回缓存值
if (returnNow) {
    return Promise.resolve(state.data);
}
// 执行我们自定义的传入的 onBefore 函数
this.options.onBefore?.(params);

onBefore 阶段可以理解为在发起真正请求之前做的事情,首先说一下 stopNow、returnNow 分别代表了什么

  • stopNow:ready(可以在option中配置,默认为true) 为 false 时,stopNow 为 true,不会发出请求,直接返回
  • returnNow:该值在开启缓存时,并且缓存没有过期时为 true,这样就会把缓存的值返回

OnBefore 执行流程就是 判读是否需要请求 -> 更新 state -> 是否缓存可用 -> 自定义 onBefore 函数

onRequest

// 读取缓存
let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);
// 未使用缓存就执行 service
if (!servicePromise) {
    servicePromise = this.serviceRef.current(...params);
}

const res = await servicePromise;

onSuccess

// 判断是否 cancel 了
if (currentCount !== this.count) {
    // prevent run.then when request is canceled
    return new Promise(() => {
    });
}
// 如果成功就更新 state,执行自定义传入的 onSuccess 方法
this.setState({
    data: res,
    error: undefined,
    loading: false,
});

this.options.onSuccess?.(res, params);
this.runPluginHandler('onSuccess', res, params);

onSuccess 阶段就是请求成功后的阶段,我们会发现很多地方用到了 currentCount !== this.count 这个判断, 这个判断实际上就是用来校验当前的 runAsync 是否被打断了,如果在执行 runAsync 的时候,我们手动通过 cancel 去取消,那么就会通过这个校验,结束本次执行。

onFinally

this.options.onFinally?.(params, res, undefined);
// 判断是否 cancel 了
if (currentCount === this.count) {
    this.runPluginHandler('onFinally', params, res, undefined);
}

return res;

onError 错误处理

// 判断是否 cancel 了
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);
// 即使捕获到错误 onFinally 依然正常最后执行
this.options.onFinally?.(params, undefined, error);
if (currentCount === this.count) {
    this.runPluginHandler('onFinally', params, undefined, error);
}

throw error;

当我们的请求抛出异常被 catch 时,onError 阶段触发,首先更新 state,然后看我们是否自定义处理异常,有则执行,最后执行 onFinally, 因为无论请求成功与否,finally阶段都要触发。

run

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

run本质就是执行 runAsync ,只不过不需要我们再手动处理异常了。

cancel

cancel() {
    this.count += 1;
    this.setState({
      loading: false,
    });
    this.runPluginHandler('onCancel');
}

cancel 就是在 runAsync 说到的手动取消的方法,他改变了 count 的值,导致 count 和 currentCount 不相等,跳出 runAsync。

refresh

refresh() {
    this.run(...(this.state.params || []));
}

refresh 刷新,使用上次请求的参数重新请求一次。

refreshAsync

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

refresh 对应 run ,refreshAsync 对应 runAsync。

mutate

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

修改 data 的值,传入的可以是个值,也可以是个函数。

总结

我们来梳理一下 useRequest 到底做了什么,他本身返回的是 useRequestImplement 执行结果,传入了真正的请求方法,用户自定义配置,以及一个插件数组,
而 useRequestImplement 又依赖于 Fetch 这个构造方法。整个请求过程都有一条时间线,插件执行的结果就是每个时间节点对应要做的操作,通过 runPluginHandler
来统一触发执行,在有的节点允许用户自定义要执行的方法,关于请求的数据的被存在 Fetch 的 state 中,是不是有点像 Redux ,最后再将用户操作和请求有关的数据暴露出去。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
@RequestBody注解用于将请求体中的数据绑定到方法的参数上。其源码实现主要涉及到RequestResponseBodyMethodProcessor类。该类实现了HandlerMethodArgumentResolver接口,具体负责处理方法参数的解析。 在RequestResponseBodyMethodProcessor类中,supportsParameter方法用于判断是否支持解析某个参数。它通过判断方法参数上是否存在@RequestBody注解来进行判断。如果存在该注解,则返回true,表示该参数可被解析为请求体数据。 resolveArgument方法则是具体进行参数解析的方法。它首先会获取请求的Content-Type,判断是否为application/json或application/xml等类型,以确保请求体的数据为JSON或XML格式。然后将请求体的数据转换为方法参数所需的对象。这里会使用HttpMessageConverter进行数据的转换,将请求体的数据转换为方法参数对应的对象。 综上所述,@RequestBody注解的源码实现主要通过RequestResponseBodyMethodProcessor类来实现,它负责判断是否支持解析参数以及将请求体数据转换为方法参数所需的对象。<span class="em">1</span><span class="em">2</span> #### 引用[.reference_title] - *1* *2* [Spring 注解面面通 之 @RequestBody参数绑定源码解析](https://blog.csdn.net/securitit/article/details/110705815)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值