ahooks中的核心hook-useRequest(上)

1,755 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

前言

useRequest是一个异步数据管理的hooks,是ahooks Hooks库的核心hook,因为其通过插件式组织代码,大部分功能都通过插件的形式来实现,所以其核心代码行数较少,简单易懂,还可以支持我们自定义扩展功能。可以说,useRequest能处理React项目绝大多数的网络请求场景。

让咱自己写可能写不出来,那就先从模仿开始,通过阅读useRequest的代码,从中学习大佬们的代码逻辑和思维处理。

ahooks: ahooks.js.org/

文中代码基于3.7.2版本

前置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 init
    const params = fetchInstance.state.params ?? [];
    // @ts-ignore
    fetchInstance.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 canceled
        return new Promise(() => {});
      }
      this.setState({
        error,
        loading: false,
      });

      throw error;
    }
  }

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

  refreshAsync() {
    // @ts-ignore
    return 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-ignore
      fetchInstance.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-ignore
    this.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(下)