ahooks 源码解读系列 - 14

869 阅读7分钟

这个系列是将 ahooks 里面的所有 hook 源码都进行解读,通过解读 ahooks 的源码来熟悉自定义 hook 的写法,提高自己写自定义 hook 的能力,希望能够对大家有所帮助。

为了和代码原始注释区分,个人理解部分使用 ///开头,此处和 三斜线指令没有关系,只是为了做区分。

往期回顾

开学啦,开学啦~
终于来到了 Async 部分,个人觉得这部分是 ahooks 里面最复杂的,大家做好准备了嘛?那我们就开始吧~

Async

useRequest

“一站式异步请求服务”

一个强大的管理异步数据请求的 Hook。内部整合了 loadmore、paginated 等常用功能。

useAsync

import debounce from 'lodash.debounce';
import throttle from 'lodash.throttle';
import { useCallback, useEffect, useRef, useState } from 'react';
import {
  BaseOptions,
  BaseResult,
  FetchConfig,
  Fetches,
  FetchResult,
  noop,
  Options,
  OptionsWithFormat,
  Service,
  Subscribe,
} from './types';
import { isDocumentVisible } from './utils';
import { getCache, setCache } from './utils/cache';
import limit from './utils/limit';
import usePersistFn from './utils/usePersistFn';
import useUpdateEffect from './utils/useUpdateEffect';
import subscribeFocus from './utils/windowFocus';
import subscribeVisible from './utils/windowVisible';

const DEFAULT_KEY = 'AHOOKS_USE_REQUEST_DEFAULT_KEY';

/// 个人认为将 fetch 单独抽离出来的作用有:
/// 可以对 service 做 debounce、throttle、poll等相关功能的扩展
/// 可以维持一些只与请求相关的状态、数据,脱离 react 的生命周期,使得可以在多个不同 hook 之间同步状态
/// 其实上面两条从功能上讲,写在 hook 里面也是可以的,但是从代码上来说,这样抽离出来能够更好的分离关注点,代码组织也更合理
class Fetch<R, P extends any[]> {
  config: FetchConfig<R, P>;

  service: Service<R, P>;

  // 请求时序
  count = 0;

  // visible 后,是否继续轮询
  pollingWhenVisibleFlag = false;

  pollingTimer: any = undefined;

  loadingDelayTimer: any = undefined;

  subscribe: Subscribe<R, P>;

  unsubscribe: noop[] = [];

  that: any = this;

  state: FetchResult<R, P> = {
    loading: false,
    params: [] as any,
    data: undefined,
    error: undefined,
    run: this.run.bind(this.that),
    mutate: this.mutate.bind(this.that),
    refresh: this.refresh.bind(this.that),
    cancel: this.cancel.bind(this.that),
    unmount: this.unmount.bind(this.that),
  };

  debounceRun: any;

  throttleRun: any;

  limitRefresh: any;

  constructor(
    service: Service<R, P>,/// 请求方法
    config: FetchConfig<R, P>,/// 请求配置
    subscribe: Subscribe<R, P>,/// 外部的 fetch 状态改变监听函数
    initState?: { data?: any; error?: any; params?: any; loading?: any },/// 初始值
  ) {
    this.service = service;
    this.config = config;
    this.subscribe = subscribe;
    if (initState) {
      this.state = {
        ...this.state,
        ...initState,
      };
    }

    this.debounceRun = this.config.debounceInterval /// 如果设置了debounceInterval 就对debounceRun进行赋值
      ? debounce(this._run, this.config.debounceInterval)
      : undefined;
    this.throttleRun = this.config.throttleInterval
      ? throttle(this._run, this.config.throttleInterval)
      : undefined;
    this.limitRefresh = limit(this.refresh.bind(this), this.config.focusTimespan);

    if (this.config.pollingInterval) {/// subscribeVisible 方法会返回解除绑定的方法,然后将该方法放入 unsubscribe 数组中
      this.unsubscribe.push(subscribeVisible(this.rePolling.bind(this)));
    }
    if (this.config.refreshOnWindowFocus) {
      this.unsubscribe.push(subscribeFocus(this.limitRefresh.bind(this)));
    }
  }

  setState(s = {}) {/// 使用 assign 的方式更新
    this.state = {
      ...this.state,
      ...s,
    };
    this.subscribe(this.state); /// 每次更新都调用 subscribe 方法,在hook中使用这个机制更新 fetchs  
  }

  _run(...args: P) {/// 核心方法,管理请求的发起
    // 取消已有定时器
    if (this.pollingTimer) {
      clearTimeout(this.pollingTimer);
    }
    // 取消 loadingDelayTimer
    if (this.loadingDelayTimer) {
      clearTimeout(this.loadingDelayTimer);
    }
    this.count += 1;
    // 闭包存储当次请求的 count
    const currentCount = this.count;

    this.setState({
      loading: !this.config.loadingDelay,
      params: args,
    });

    if (this.config.loadingDelay) {/// 和上面的 setState 结合设置 loading 状态
      this.loadingDelayTimer = setTimeout(() => {
        this.setState({
          loading: true,
        });
      }, this.config.loadingDelay);
    }

    return this.service(...args)
      .then((res) => {
        /// cancel 时 count +1 了,此时返回一个永远 pendding 的 promise
        if (currentCount !== this.count) {
          // prevent run.then when request is canceled
          return new Promise(() => {});
        }
        if (this.loadingDelayTimer) {
          clearTimeout(this.loadingDelayTimer);
        }
        const formattedResult = this.config.formatResult ? this.config.formatResult(res) : res;
        this.setState({/// 将成功结果存入state中
          data: formattedResult,
          error: undefined,
          loading: false,
        });
        if (this.config.onSuccess) {
          this.config.onSuccess(formattedResult, args);
        }
        return formattedResult;
      })
      .catch((error) => {
        if (currentCount !== this.count) {
          // prevent run.then when request is canceled
          return new Promise(() => {});
        }
        if (this.loadingDelayTimer) {
          clearTimeout(this.loadingDelayTimer);
        }
        this.setState({/// 失败结果存入 state
          data: undefined,
          error,
          loading: false,
        });
        if (this.config.onError) {
          this.config.onError(error, args);
        }
        // If throwOnError, user should catch the error self,
        // or the page will crash
        if (this.config.throwOnError) {
          throw error;
        }
        console.error(error);
        // eslint-disable-next-line prefer-promise-reject-errors
        return Promise.reject(
          'useRequest has caught the exception, if you need to handle the exception yourself, you can set options.throwOnError to true.',
        );
      })
      .finally(() => {
        if (currentCount === this.count) {/// 在请求结束之后,处理轮询相关的逻辑
          if (this.config.pollingInterval) {
            // 如果屏幕隐藏,并且 !pollingWhenHidden, 则停止轮询,并记录 flag,等 visible 时,继续轮询
            if (!isDocumentVisible() && !this.config.pollingWhenHidden) {
              this.pollingWhenVisibleFlag = true;
              return;
            }
            this.pollingTimer = setTimeout(() => {
              this._run(...args);
            }, this.config.pollingInterval);
          }
        }
      });
  }

  run(...args: P) {/// 对外暴露的run方法,先判断是否配置了debounceRun,再判断是否配置了throttleRun,两种场景都只会返回一个空的promise
    if (this.debounceRun) {
      this.debounceRun(...args);
      // TODO 如果 options 存在 debounceInterval,或 throttleInterval,则 run 和 refresh 不会返回 Promise。 带类型需要修复后,此处变成 return;。
      return Promise.resolve(null as any);
    }
    if (this.throttleRun) {
      this.throttleRun(...args);
      return Promise.resolve(null as any);
    }
    return this._run(...args);
  }

  cancel() {/// 取消请求
    if (this.debounceRun) {
      this.debounceRun.cancel();
    }
    if (this.throttleRun) {
      this.throttleRun.cancel();
    }
    if (this.loadingDelayTimer) {
      clearTimeout(this.loadingDelayTimer);
    }
    if (this.pollingTimer) {
      clearTimeout(this.pollingTimer);
    }
    this.pollingWhenVisibleFlag = false;

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

  refresh() {/// 重新请求
    return this.run(...this.state.params);
  }

  rePolling() {/// 重新轮询
    if (this.pollingWhenVisibleFlag) {
      this.pollingWhenVisibleFlag = false;
      this.refresh();
    }
  }

  mutate(data: any) {/// 直接设置 data 的值
    if (typeof data === 'function') {
      this.setState({
        // eslint-disable-next-line react/no-access-state-in-setstate
        data: data(this.state.data) || {},
      });
    } else {
      this.setState({
        data,
      });
    }
  }

  unmount() {
    this.cancel();
    this.unsubscribe.forEach((s) => {
      s();
    });
  }
}
/// 提供出来的管理异步请求的hook,专注于fetch的管理,service的管理放在了fetch中进行
function useAsync<R, P extends any[], U, UU extends U = any>(
  service: Service<R, P>,
  options: OptionsWithFormat<R, P, U, UU>,
): BaseResult<U, P>;
function useAsync<R, P extends any[]>(
  service: Service<R, P>,
  options?: BaseOptions<R, P>,
): BaseResult<R, P>;
function useAsync<R, P extends any[], U, UU extends U = any>(
  service: Service<R, P>,
  options?: Options<R, P, U, UU>,
): BaseResult<U, P> {
  const _options = options || ({} as Options<R, P, U, UU>);
  const {
    refreshDeps = [],
    manual = false,
    onSuccess = () => {},
    onError = () => {},

    defaultLoading = false,
    loadingDelay,

    pollingInterval = 0,
    pollingWhenHidden = true,

    defaultParams = [],
    refreshOnWindowFocus = false,
    focusTimespan = 5000,
    fetchKey,
    cacheKey,
    cacheTime = 5 * 60 * 1000,
    staleTime = 0,
    debounceInterval,
    throttleInterval,
    initialData,
    ready = true,
    throwOnError = false,
  } = _options; /// 取出各个配置项,并设置默认值

  const newstFetchKey = useRef(DEFAULT_KEY);

  // 持久化一些函数
  const servicePersist = usePersistFn(service) as any;

  const onSuccessPersist = usePersistFn(onSuccess);

  const onErrorPersist = usePersistFn(onError);

  const fetchKeyPersist = usePersistFn(fetchKey);

  let formatResult: any;
  if ('formatResult' in _options) {
    // eslint-disable-next-line prefer-destructuring
    formatResult = _options.formatResult;
  }
  const formatResultPersist = usePersistFn(formatResult);

  const config = {
    formatResult: formatResultPersist,
    onSuccess: onSuccessPersist,
    onError: onErrorPersist,
    loadingDelay,
    pollingInterval,
    pollingWhenHidden,
    // refreshOnWindowFocus should not work on manual mode
    refreshOnWindowFocus: !manual && refreshOnWindowFocus,
    focusTimespan,
    debounceInterval,
    throttleInterval,
    throwOnError,
  }; /// 组装 Fetch 需要的 config

  const subscribe = usePersistFn((key: string, data: any) => {
    setFetches((s) => {
      // eslint-disable-next-line no-param-reassign
      s[key] = data;
      return { ...s };
    });
  }) as any;/// 订阅 fetch 的变更

  const [fetches, setFetches] = useState<Fetches<U, P>>(() => {
    // 如果有 缓存,则从缓存中读数据
    if (cacheKey) {
      const cacheData = getCache(cacheKey)?.data;
      if (cacheData) {
        newstFetchKey.current = cacheData.newstFetchKey;
        /* 使用 initState, 重新 new Fetch */
        const newFetches: any = {};
        Object.keys(cacheData.fetches).forEach((key) => {
          const cacheFetch = cacheData.fetches[key];
          /// 使用最新的配置和监听函数作为配置参数,同时使用缓存的数据作为初始值来生成一个新的fetch
          /// subscribe.bind(null, key) 使用 bind 生成一个偏函数 ,将 subscribe 的第一个参数固定为 key
          const newFetch = new Fetch(servicePersist, config, subscribe.bind(null, key), {
            loading: cacheFetch.loading,
            params: cacheFetch.params,
            data: cacheFetch.data,
            error: cacheFetch.error,
          });
          newFetches[key] = newFetch.state;
        });
        return newFetches;
      }
    }
    return {};
  });

  const fetchesRef = useRef(fetches);/// 持久化 fetchs
  fetchesRef.current = fetches;

  const readyMemoryParams = useRef<P>();

  /// 注意,此处是没有进行 debounce 等操作的,真正的 debounce 操作在 fetch 内部进行
  /// 我们很容易遇到的一个场景是对输入进行 debounce 搜索
  /// 如果此时将搜索值作为 fetchKey ,例如:fetchKey: (searchWord?: string) => searchWord || '', 则会导致 debounce 失去预期作用!!!
  /// 因为每个输入值都将生成一个新的 fetch,而导致 debounce 功能和预期不符
  const run = useCallback(
    (...args: P) => {
      if (!ready) {
        // 没有 ready, 记录请求参数,等 ready 后,发起请求用
        readyMemoryParams.current = args;
        return;
      }

      if (fetchKeyPersist) {
        const key = fetchKeyPersist(...args);
        newstFetchKey.current = key === undefined ? DEFAULT_KEY : key;
      }
      const currentFetchKey = newstFetchKey.current;
      // 这里必须用 fetchsRef,而不能用 fetches。
      // 否则在 reset 完,立即 run 的时候,这里拿到的 fetches 是旧的。
      let currentFetch = fetchesRef.current[currentFetchKey];
      if (!currentFetch) {/// 当前 key 对应的请求还一次都没有发起过,新建一个请求,并且将这个请求放入 fetchs 中
        const newFetch = new Fetch(servicePersist, config, subscribe.bind(null, currentFetchKey), {
          data: initialData,
        });
        currentFetch = newFetch.state;
        setFetches((s) => {
          // eslint-disable-next-line no-param-reassign
          s[currentFetchKey] = currentFetch;
          return { ...s };
        });
      }
      return currentFetch.run(...args);/// 否则直接run对应的fetch
    },
    [fetchKey, subscribe, ready],
  );
  const runRef = useRef(run);
  runRef.current = run;

  // cache
  useUpdateEffect(() => {
    if (cacheKey) {
      setCache(cacheKey, cacheTime, {
        fetches,
        newstFetchKey: newstFetchKey.current,
      });
    }
  }, [cacheKey, fetches]);/// 更新缓存

  // for ready
  const hasTriggeredByReady = useRef(false);
  useUpdateEffect(() => {
    if (ready) {
      if (!hasTriggeredByReady.current && readyMemoryParams.current) {
        runRef.current(...readyMemoryParams.current);
      }
      hasTriggeredByReady.current = true;
    }
  }, [ready]);/// ready 之后立马使用之前记录下来的参数发一次请求

  // 第一次默认执行
  useEffect(() => {
    if (!manual) {
      // 如果有缓存,则重新请求
      if (Object.keys(fetches).length > 0) {
        // 如果 staleTime 是 -1,则 cache 永不过期
        // 如果 statleTime 超期了,则重新请求
        const cacheStartTime = (cacheKey && getCache(cacheKey)?.startTime) || 0;
        if (!(staleTime === -1 || new Date().getTime() - cacheStartTime <= staleTime)) {
          /* 重新执行所有的 cache */
          Object.values(fetches).forEach((f) => {
            f.refresh();
          });
        }
      } else {
        // 第一次默认执行,可以通过 defaultParams 设置参数
        runRef.current(...(defaultParams as any));
      }
    }
  }, []);

  // 重置 fetches
  const reset = useCallback(() => {
    Object.values(fetchesRef.current).forEach((f) => {
      f.unmount();
    });
    newstFetchKey.current = DEFAULT_KEY;
    setFetches({});
    // 不写会有问题。如果不写,此时立即 run,会是老的数据
    /// 因为 run 里面的这一行代码:let currentFetch = fetchesRef.current[currentFetchKey];
    fetchesRef.current = {};
  }, [setFetches]);

  //  refreshDeps 变化,重新执行所有请求
  useUpdateEffect(() => {
    if (!manual) {/// 如果设置了 manual 就不会自动请求了
      /* 全部重新执行 */
      Object.values(fetchesRef.current).forEach((f) => {
        f.refresh();
      });
    }
  }, [...refreshDeps]);

  // 卸载组件触发
  useEffect(
    () => () => {
      Object.values(fetchesRef.current).forEach((f) => {
        f.unmount();
      });
    },
    [],
  );

  const notExecutedWarning = useCallback(
    (name: string) => () => {
      console.warn(`You should't call ${name} when service not executed once.`);
    },
    [],
  );

  return {
    loading: (ready && !manual) || defaultLoading,
    data: initialData,
    error: undefined,
    params: [],
    /// 在 fetches 为空,也就是 service 还没有触发的时候,调用这三个方法只会得到一个警告
    /// 在 service 触发了之后,目标 fetch 中的对应方法就会覆盖掉这三个方法
    cancel: notExecutedWarning('cancel'),
    refresh: notExecutedWarning('refresh'),
    mutate: notExecutedWarning('mutate'),

    ...((fetches[newstFetchKey.current] as FetchResult<U, P> | undefined) || {}),
    run,
    fetches,
    reset,
  } as BaseResult<U, P>;
}

export default useAsync;

以上内容由于本人水平问题难免有误,欢迎大家进行讨论反馈。