前言
useRequest是一个异步数据管理的hooks,是ahooks Hooks库的核心hook,因为其通过插件式组织代码,大部分功能都通过插件的形式来实现,所以其核心代码行数较少,简单易懂,还可以支持我们自定义扩展功能。可以说,useRequest能处理React项目绝大多数的网络请求场景。
让咱自己写可能写不出来,那就先从模仿开始,通过阅读useRequest的代码,从中学习大佬们的代码逻辑和思维处理。
前置hook了解
在useRequest的源码实现中使用到了一些其他的hooks
- useCreation:useMemo 或 useRef 的替代品。
- useLatest:返回当前最新值的 Hook, 可以避免闭包问题。
- useMemoizedFn:useCallback的替代品 。
- useMount:只在组件初始化时执行的 Hook。
- useUnmount:在组件卸载(unmount)时执行的 Hook。
- useUpdate:强制组件重新渲染的hook。
实现最基础的useRequest Hook
const { data, error, loading, cancel} = useRequest(service);
useRequest通过定义一个class类Fetch 来维护相关的数据(data,loading等)和方法(run, refresh等)然后在useRequestImplement中创建Fetch实例,并返回实例属性和方法。
我对代码进行了拆分,保留了useRequest中核心的功能,该hook接收一个promise,需要返回data、error、loading、cancel状态。
const useRequestImplement = (service: Promise<any>) => {const serviceRef = useLatest(service);const update = useUpdate();const fetchInstance = useCreation(() => {return new Fetch(serviceRef, update);}, []);useMount(() => {// useCachePlugin can set fetchInstance.state.params from cache when initconst params = fetchInstance.state.params ?? [];// @ts-ignorefetchInstance.runAsync(...params);});useUnmount(() => {fetchInstance.cancel();});return {loading: fetchInstance.state.loading,data: fetchInstance.state.data,error: fetchInstance.state.error,params: fetchInstance.state.params || [],cancel: useMemoizedFn(fetchInstance.cancel.bind(fetchInstance)),};
};
export default class Fetch<TData, TParams extends any[]> {public count: number = 0;public state = {loading: false,params: undefined,data: undefined,error: undefined,};constructor(public serviceRef, public subscribe) {}setState(s) {this.state = {...this.state,...s,};this.subscribe();}async runAsync(...params: TParams): Promise<any> {this.count += 1;const currentCount = this.count;this.setState({loading: true,params,});try {const servicePromise = this.serviceRef.current(...params);const res = await servicePromise;if (currentCount !== this.count) {return new Promise(() => {});}this.setState({data: res,error: undefined,loading: false,});return res;} catch (error) {if (currentCount !== this.count) {// prevent run.then when request is canceledreturn new Promise(() => {});}this.setState({error,loading: false,});throw error;}}cancel() {this.count += 1;this.setState({loading: false,});}refreshAsync() {// @ts-ignorereturn this.runAsync(...(this.state.params || []));}
}
实现剩余核心功能
接收用户的自定义配置,包括manual(手动模式)和一些回调函数(onBefore,onSuccess, onError,onFinally)
const useRequestImplement = (service: Promise<any>, options) => {const { manual = false, ...rest } = options;const fetchOptions = {manual,...rest,};// const serviceRef = useLatest(service);// const update = useUpdate();// const fetchInstance = useCreation(() => {return new Fetch(serviceRef, fetchOptions, update);// }, []);fetchInstance.options = fetchOptions;useMount(() => {if (!manual) {const params = fetchInstance.state.params || options.defaultParams || [];// @ts-ignorefetchInstance.run(...params);}});// useUnmount(() => {// fetchInstance.cancel();// });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)),};
};
export default class Fetch<TData, TParams extends any[]> {
// public count: number = 0;
//
// public state = {
// loading: false,
// params: undefined,
// data: undefined,
// error: undefined,
// };constructor(public serviceRef, public options, public subscribe) {this.state = {...this.state,loading: !options.manual,};}// setState(s) {// this.state = {// ...this.state,// ...s,// };// this.subscribe();// }
// async runAsync(...params: TParams): Promise<any> {
// this.count += 1;
// const currentCount = this.count;
//
// this.setState({
// loading: true,
// params,
// });
// this.options.onBefore?.(params);
// try {
// const servicePromise = this.serviceRef.current(...params);
// const res = await servicePromise;
// if (currentCount !== this.count) {
// return new Promise(() => {});
// }
// this.setState({
// data: res,
// error: undefined,
// loading: false,
// });
// this.options.onSuccess?.(res, params);this.options.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.options.onFinally?.(params, undefined, error);
// throw error;
// }
// }
// cancel() {
// this.count += 1;
// this.setState({
// loading: false,
// });
// }
//
// refreshAsync() {
// // @ts-ignore
// return this.runAsync(...(this.state.params || []));
// }run(...params: TParams) {this.runAsync(...params).catch((error) => {if (!this.options.onError) {console.error(error);}});}refresh() {// @ts-ignorethis.run(...(this.state.params || []));}mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {const targetData = isFunction(data) ? data(this.state.data) : data;this.setState({data: targetData,});}
}
runAsync和run
runAsync方法返回一个promise,使用runAsync时,当请求报错会中断后续操作,需要手动捕获异常。
run方法则对runAsync进行了封装,帮助我们了捕获异常,或可以通过options.onError来处理异常行为。
refresh和refreshAsync
useRequest维护了一份params,调用run()和runAsync()的时候会同时更新params。以便给refresh和refreshAsync方法使用
cancel
useRequest维护了一个count。
而runAsync方法本身也维护一个currentCount。
每次调用runAsync时,count进行一次++操作,然后将其赋值给currentCount。
每次cancel方法count会再进行一次++操作。通过比较count和currentCount的值来判断用户是否进行了取消操作,进行相应的处理
mutate
支持立即修改 useRequest 返回的 data 参数。
mutate 的用法与 React.setState 一致,支持 mutate(newData) 和 mutate((oldData) => newData) 两种写法。
小结
以上是useRequest hook 的基本功能,剩余功能如loading状态延时、请求防抖、节流、数据缓存等功能都是通过插件的形式进行实现的,具体实现可以看 ahooks中的核心hook-useRequest(下)
最后
整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。
有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享
部分文档展示:
文章篇幅有限,后面的内容就不一一展示了
有需要的小伙伴,可以点下方卡片免费领取