前言
useRequest是 ahooks 中提供的用于异步数据管理的 Hook
useRequest通过插件式组织代码,核心代码极其简单,并且可以很方便的扩展出更高级的功能。目前已有能力包括:
- 自动请求/手动请求
 - 轮询
 - 防抖
 - 节流
 - 屏幕聚焦重新请求
 - 错误重试
 - loading delay
 - SWR(stale-while-revalidate)
 - 缓存
 
基本用法
默认请求
默认情况下,useRequest 第一个参数是一个异步函数,只需要返回一个 promise 即可。在组件初始化时,会自动执行该异步函数。同时自动管理该异步函数的 loading , data , error 等状态。
            
            
              go
              
              
            
          
          const { data, error, loading } = useRequest(service);
        手动触发
如果设置了 options.manual = true,则 useRequest 不会默认执行,需要通过 run 或者 runAsync 来触发执行。
            
            
              arduino
              
              
            
          
          const { loading, run, runAsync } = useRequest(service, {
    manual: true
});
<button onClick={run} disabled={loading}>
    {loading ? 'Loading' : 'Edit'}
</button>
        run 与 runAsync 的区别在于:
- 
run是一个普通的同步函数,自动捕获异常,可以通过options.onError来处理异常时的行为。 - 
runAsync是一个返回Promise的异步函数,如果使用runAsync来调用,则意味着需要手动捕获异常。 
生命周期
useRequest 提供了以下几个生命周期配置项,以便在异步函数的不同阶段做一些处理。
onBefore:请求之前触发onSuccess:请求成功触发onError:请求失败触发onFinally:请求完成触发
            
            
              javascript
              
              
            
          
          const { loading, run } = useRequest(service, {
    manual: true,
    onBefore: (params) => {
        console.log(params);
    },
    onSuccess: (result, params) => {
        console.log(result, params);
    },
    onError: (error) => { },
    onFinally: (params, result, error) => { },
});
        刷新(重复上一次请求)
useRequest 提供了 refresh 和 refreshAsync 方法,可以使用上一次的参数,重新发起请求。
            
            
              typescript
              
              
            
          
          const { data, loading, run, refresh } = useRequest((id: number) => getUsername(id), {
    manual: true,
});
        refresh 和 refreshAsync 的区别和 run 和 runAsync 一样。
立即变更数据
useRequest 提供了 mutate, 支持立即修改 useRequest 返回的 data 参数。
支持 mutate(newData) 和 mutate((oldData) => newData) 两种写法。
mutate 的应用场景:
不希望等接口调用成功之后,才给用户反馈。而是直接修改页面数据,同时在背后去调用修改接口,等修改接口返回之后,另外提供反馈。
取消响应
useRequest 提供了 cancel 函数,用于忽略当前 promise 返回的数据和错误
需要注意的是:调用 cancel 函数并不会取消 promise 的执行
同时 useRequest 会在以下时机自动忽略响应:
- 组件卸载时,忽略正在进行的 promise
 - 竞态取消,当上一次 promise 还没返回时,又发起了下一次 promise,则会忽略上一次 promise 的响应
 
参数管理
useRequest 返回的 params 会记录当次调用 service 的参数数组。如触发了 run(1, 2, 3),则 params 等于 [1, 2, 3] 。
如果设置了 options.manual = false,则首次调用 service 的参数可以通过 options.defaultParams 来设置。
轮询
通过设置 options.pollingInterval,进入轮询模式,useRequest 会定时触发 service 执行。
            
            
              arduino
              
              
            
          
          const { data, run, cancel } = useRequest(getUsername, {
  pollingInterval: 3000,
});
        通过 cancel 来停止轮询,通过 run/runAsync 来启动轮询。
依赖刷新
useRequest 提供了一个 options.refreshDeps 参数,当它的值变化后,会重新触发请求。
            
            
              scss
              
              
            
          
          const [userId, setUserId] = useState('1');
const { data, run } = useRequest(() => getUserSchool(userId), {
  refreshDeps: [userId],
});
        错误重试
通过设置 options.retryCount,指定错误重试次数,则 useRequest 在失败后会进行重试。
            
            
              arduino
              
              
            
          
          const { data, run } = useRequest(getUsername, {
  retryCount: 3,
});
        缓存 & SWR
如果设置了 options.cacheKey,useRequest 会将当前请求成功的数据缓存起来。下次组件初始化时,如果有缓存数据,会优先返回缓存数据,然后在背后发送新请求,也就是 SWR 。
通过 options.staleTime 设置数据保持新鲜时间,在该时间内,则认为数据是新鲜的,不会重新发起请求。
通过 options.cacheTime 设置数据缓存时间,超过该时间,会清空该条缓存数据。
            
            
              php
              
              
            
          
          const { data, loading } = useRequest(getArticle, {
    cacheKey: 'staleTime-demo',
    staleTime: 5000,
  });
        需要注意的是,同一个 cacheKey 的内容,在全局是共享的,这会带来以下几个特性
- 请求 
Promise共享,相同的cacheKey同时只会有一个在发起请求,后发起的会共用同一个请求Promise - 数据同步,任何时候,当改变其中某个 
cacheKey的内容时,其它相同cacheKey的内容均会同步 
ahooks 提供了一个 clearCache 方法,通过 clearCache 方法,可以清除指定 cacheKey 的缓存数据。
源码解析
正如前文所述,useRequest 源码非常简单。下面来看一下。
入口
            
            
              swift
              
              
            
          
          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 会将第三个参数作为 Plugin 传入。下面来看一下 Implement。
useRequestImplement
            
            
              scss
              
              
            
          
          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(() => {
    const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
    return new Fetch<TData, TParams>(
      serviceRef,
      fetchOptions,
      update,
      Object.assign({}, ...initState),
    );
  }, []);
  fetchInstance.options = fetchOptions;
  // run all plugins hooks
  fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
  useMount(() => {
    if (!manual) {
      // useCachePlugin can set fetchInstance.state.params from cache when init
      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)),
  } as Result<TData, TParams>;
}
        大体逻辑如下:
- 创建请求实例 
fetchInstance,后续如果需要再请求,则通过实例运行,同时通过fetchInstance管理请求参数以及请求状态。创建fetchInstance时会传入fetchOptions执行插件的onInit方法,并将onInit方法的返回值保存到fetchInstance上。 - 传入 
fetchInstance以及fetchOptions执行传入的 plugin,保存至fetchInstance的pluginImpls。 - 如果没有设置 
manual为true,则执行请求。 
核心 Fetch Class
可以看到, useRequest 的核心就是 fetchInstance。 通过 fetchInstance 管理了请求参数,请求状态,竞态条件等等。下面一起来看一下 Fetch。
            
            
              kotlin
              
              
            
          
          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>> = {}) {
    this.state = {
      ...this.state,
      ...s,
    };
    this.subscribe();
  }
  runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
    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(() => {});
      }
      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() {
    this.run(...(this.state.params || []));
  }
  refreshAsync() {
    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,
    });
  }
        可以看到,逻辑非常简单
在 ts 中,constructor 的入参中,带有可见性修饰符的参数会自动挂载到实例上。
Fetch 的核心就是 runPluginHandler 和 runAsync 方法。
runPluginHandler 负责执行插件中特定的生命周期。
runAsync 负责请求,每一次执行 runAsync 方法,都会将 this.count + 1,并将当前的id保存下来,这个 count 其实就是请求的id。在后续的处理中,如果不是需要的id,则通过一个 pending 的 promise 忽略返回结果。同时会在特定的时机执行特定的生命周期函数(包括 useRequest 入参的生命周期和插件返回的生命周期。
插件
useRequest 的插件是一个函数,入参为 fetchInstance 及 useRequest 的 options,返回一个生命周期对象,指定的函数会在请求对应的时机执行。
插件的 onInit 方法可以在初始化的时候修改 fetchInstance.state。
            
            
              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;
}
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>>;
};
        useRequest 提供的各种功能都是通过插件实现的。
下面来看一下缓存 & (SWR)的实现
缓存 & (SWR)
swr的实现是通过 useCachePlugin 实现的。 useCachePlugin 的核心方法如下。
            
            
              ini
              
              
            
          
          const cache = new Map<CachedKey, RecordData>();
const setCache = (key: CachedKey, cacheTime: number, cachedData: CachedData) => {
  const currentCache = cache.get(key);
  if (currentCache?.timer) {
    clearTimeout(currentCache.timer);
  }
  let timer: Timer | undefined = undefined;
  if (cacheTime > -1) {
    // if cache out, clear it
    timer = setTimeout(() => {
      cache.delete(key);
    }, cacheTime);
  }
  cache.set(key, {
    ...cachedData,
    timer,
  });
};
const getCache = (key: CachedKey) => {
  return cache.get(key);
};
const clearCache = (key?: string | string[]) => {
  if (key) {
    const cacheKeys = Array.isArray(key) ? key : [key];
    cacheKeys.forEach((cacheKey) => cache.delete(cacheKey));
  } else {
    cache.clear();
  }
};
        通过一个共享 Map 对象保存请求的数据,缓存的时间,在后续发起请求时,如果数据有效,返回并终止请求,如果数据无效,返回缓存的数据并请求新的数据。同时由于是使用 Map 缓存,这意味着只要能保持引用稳定,可以使用任意 js 对象作为 key。核心逻辑如下:
            
            
              javascript
              
              
            
          
            return {
    onBefore: (params) => {
      const cacheData = _getCache(cacheKey, params);
      if (!cacheData || !Object.hasOwnProperty.call(cacheData, 'data')) {
        return {};
      }
      // If the data is fresh, stop request
      if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
        return {
          loading: false,
          data: cacheData?.data,
          error: undefined,
          returnNow: true,
        };
      } else {
        // If the data is stale, return data, and request continue
        return {
          data: cacheData?.data,
          error: undefined,
        };
      }
    },
    onRequest: (service, args) => {
      let servicePromise = getCachePromise(cacheKey);
      // If has servicePromise, and is not trigger by self, then use it
      if (servicePromise && servicePromise !== currentPromiseRef.current) {
        return { servicePromise };
      }
      servicePromise = service(...args);
      currentPromiseRef.current = servicePromise;
      setCachePromise(cacheKey, servicePromise);
      return { servicePromise };
    },
    onSuccess: (data, params) => {
      if (cacheKey) {
        // cancel subscribe, avoid trgger self
        unSubscribeRef.current?.();
        _setCache(cacheKey, {
          data,
          params,
          time: new Date().getTime(),
        });
        // resubscribe
        unSubscribeRef.current = subscribe(cacheKey, (d) => {
          fetchInstance.setState({ data: d });
        });
      }
    },
    onMutate: (data) => {
      if (cacheKey) {
        // cancel subscribe, avoid trigger self
        unSubscribeRef.current?.();
        _setCache(cacheKey, {
          data,
          params: fetchInstance.state.params,
          time: new Date().getTime(),
        });
        // resubscribe
        unSubscribeRef.current = subscribe(cacheKey, (d) => {
          fetchInstance.setState({ data: d });
        });
      }
    },
  };
        其他的插件也很简单,比如 防抖 和 节流 就是通过 lodash 对请求进行处理。
总结
学习了 useRequest 的使用及实现方式。 useRequest 通过插件式组织代码,核心代码极其简单,并且可以很方便的扩展出更高级的功能,而且提供了自定义的方法,如果已有的功能不能满足需求,只需要一个插件即可。