ahooks 源码解读系列 - 15

·  阅读 447
ahooks 源码解读系列 - 15

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

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

往期回顾

上一篇是不是看的脑阔痛,今天来点简单的~
下面让我们一起将 useRequest 剩下的部分看完、

useRequest

使用 umi-request 对传入的 service 进行封装,然后传给 useAsync 。但是发现 index 里面并没有引入这个 hook,而且在 index 里面重复写了这部分逻辑,可能是一个忘了删的文件?

import request, { RequestOptionsInit } from 'umi-request';
import { BaseOptions, BaseResult, OptionsWithFormat } from './types';
import useAsync from './useAsync';

export type RequestService = string | ({ url: string } & RequestOptionsInit);
export type Service<P extends any[]> = RequestService | ((...args: P) => RequestService);

function useRequest<R, P extends any[], U, UU extends U = any>(
  service: Service<P>,
  options: OptionsWithFormat<R, P, U, UU>,
): BaseResult<U, P>;
function useRequest<R, P extends any[]>(
  service: Service<P>,
  options?: BaseOptions<R, P>,
): BaseResult<R, P>;
function useRequest(service: any, options?: any): any {
  let promiseService: () => Promise<any>;
  
  /// 主要就是针对传入的 service 类型不同,对 service 做不同的封装,使得最后传给 useAsync 的 service 都是一个 Promise
  if (typeof service === 'string') {
    promiseService = () => request(service);
  } else if (typeof service === 'object') {
    const { url, ...rest } = service;
    promiseService = () => request(url, rest);
  } else {
    promiseService = (...args) =>
      new Promise((resolve) => {
        const result = service(...args);
        if (typeof result === 'string') {
          request(result).then((data) => {
            resolve(data);
          });
        } else if (typeof result === 'object') {
          const { url, ...rest } = result;
          request(url, rest).then((data) => {
            resolve(data);
          });
        }
      });
  }
  return useAsync(promiseService, options);
}

export default useRequest;

复制代码

useLoadMore

加载更多,也就是说后续加载的数据需要合并到之前的返回中

import { useRef, useCallback, useMemo, useEffect, useState } from 'react';
import useAsync from './useAsync';
import {
  LoadMoreParams,
  LoadMoreOptionsWithFormat,
  LoadMoreResult,
  LoadMoreFormatReturn,
  LoadMoreOptions,
} from './types';
import useUpdateEffect from './utils/useUpdateEffect';

function useLoadMore<R extends LoadMoreFormatReturn, RR>(
  service: (...p: LoadMoreParams<R>) => Promise<RR>,
  options: LoadMoreOptionsWithFormat<R, RR>,
): LoadMoreResult<R>;
function useLoadMore<R extends LoadMoreFormatReturn, RR extends R = any>(
  service: (...p: LoadMoreParams<RR>) => Promise<R>,
  options: LoadMoreOptions<R>,
): LoadMoreResult<R>;
function useLoadMore<R extends LoadMoreFormatReturn, RR = any>(
  service: (...p: LoadMoreParams<any>) => Promise<any>,
  options: LoadMoreOptions<R> | LoadMoreOptionsWithFormat<R, RR>,
): LoadMoreResult<R> {
  const { refreshDeps = [], ref, isNoMore, threshold = 100, fetchKey, ...restOptions } = options;

  const [loadingMore, setLoadingMore] = useState(false);

  useEffect(() => {
    if (options.fetchKey) {
      console.warn("useRequest loadMore mode don't need fetchKey!");
    }
  }, []);

  const result: any = useAsync(service, {
    ...(restOptions as any),
    /// 这个 d 就是 dataGroup ,list 是所有列表请求concat的结果,也就是以上一次请求结果之后的所有数据的长度作为key
    fetchKey: (d) => d?.list?.length || 0,
    onSuccess: (...params) => {
      setLoadingMore(false);
      if (options.onSuccess) {
        options.onSuccess(...params);
      }
    },
  });

  const { data, run, params, reset, loading, fetches } = result;

  const reload = useCallback(() => {
    reset();
    const [, ...restParams] = params;
    /// 重新加载,d 为 undefined,fetchKey 将为 0
    run(undefined, ...restParams);
  }, [run, reset, params]);

  const reloadRef = useRef(reload);
  reloadRef.current = reload;
  /* loadMore 场景下,如果 refreshDeps 变化,重置到第一页 */
  useUpdateEffect(() => {
    /* 只有自动执行的场景, refreshDeps 才有效 */
    if (!options.manual) {
      reloadRef.current();
    }
  }, [...refreshDeps]);

  const dataGroup = useMemo(() => {
    let listGroup: any[] = [];
    // 在 loadMore 时,不希望清空上一次的 data。需要把最后一个 非 loading 的请求 data,放回去。
    let lastNoLoadingData: R = data;
    
    /// 这里使用 Object.values 的顺序来遍历有点问题的把?
    /// Object.values()方法返回一个给定对象自身的所有可枚举属性值的数组,值的顺序与使用for...in循环的顺序相同 ( 区别在于 for-in 循环枚举原型链中的属性 )。
    /// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/values
    /// 而 for in 遍历以任意顺序进行的,虽然大部分时候表现为按赋值顺序遍历
    /// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/for...in
    Object.values(fetches).forEach((h: any) => {
      if (h.data?.list) {
        listGroup = listGroup.concat(h.data?.list);
      }
      if (!h.loading) {
        lastNoLoadingData = h.data;
      }
    });
    return {
      ...lastNoLoadingData,
      list: listGroup,
    };
  }, [fetches, data]);

  const noMore = isNoMore ? !loading && !loadingMore && isNoMore(dataGroup) : false;

  const loadMore = useCallback(() => {
    if (noMore) {
      return;
    }
    setLoadingMore(true);
    const [, ...restParams] = params;
    /// loadMore 方法将 dataGroup 作为第一个入参传给 run 方法,所以 service 方法需要对参数进行特殊处理
    run(dataGroup, ...restParams);
  }, [noMore, run, dataGroup, params]);

  /* 上拉加载的方法 */
  const scrollMethod = () => {
    if (loading || loadingMore || !ref || !ref.current) {
      return;
    }
    if (ref.current.scrollHeight - ref.current.scrollTop <= ref.current.clientHeight + threshold) {
      loadMore();
    }
  };

  // 如果不用 ref,而用之前的 useCallbak,在某些情况下会出问题,造成拿到的 loading 不是最新的。
  // fix https://github.com/alibaba/hooks/issues/630
  const scrollMethodRef = useRef(scrollMethod);
  scrollMethodRef.current = scrollMethod;

  /* 如果有 ref,则会上拉加载更多 */
  useEffect(() => {/// 一个贴心的功能扩展
    if (!ref || !ref.current) {
      return () => {};
    }

    const scrollTrigger = () => scrollMethodRef.current();

    ref.current.addEventListener('scroll', scrollTrigger);
    return () => {
      if (ref && ref.current) {
        ref.current.removeEventListener('scroll', scrollTrigger);
      }
    };
  }, [scrollMethodRef]);

  return {
    ...result,
    data: dataGroup,
    reload,
    loading: loading && dataGroup.list.length === 0,
    loadMore,
    loadingMore,
    noMore,
  };
}

export default useLoadMore;

复制代码

usePaginated

专门处理带分页需求的请求

import { useCallback, useEffect, useMemo, useRef } from 'react';
import { Filter, PaginationConfig, Sorter } from './antdTypes';
import {
  BasePaginatedOptions,
  PaginatedFormatReturn,
  PaginatedOptionsWithFormat,
  PaginatedParams,
  PaginatedResult,
} from './types';
import useAsync from './useAsync';
import useUpdateEffect from './utils/useUpdateEffect';

function usePaginated<R, Item, U extends Item = any>(
  service: (...p: PaginatedParams) => Promise<R>,
  options: PaginatedOptionsWithFormat<R, Item, U>,
): PaginatedResult<Item>;
function usePaginated<R, Item, U extends Item = any>(
  service: (...p: PaginatedParams) => Promise<PaginatedFormatReturn<Item>>,
  options: BasePaginatedOptions<U>,
): PaginatedResult<Item>;
function usePaginated<R, Item, U extends Item = any>(
  service: (...p: PaginatedParams) => Promise<R>,
  options: BasePaginatedOptions<U> | PaginatedOptionsWithFormat<R, Item, U>,
) {
  const { paginated, defaultPageSize = 10, refreshDeps = [], fetchKey, ...restOptions } = options;

  useEffect(() => {
    if (fetchKey) {
      console.error("useRequest pagination's fetchKey will not work!");
    }
  }, []);

  const { data, params, run, loading, ...rest }: any = useAsync(service, {
    /// 默认参数里面增加分页参数
    defaultParams: [
      {
        current: 1,
        pageSize: defaultPageSize,
      },
    ],
    ...(restOptions as any),
  });

  const { current = 1, pageSize = defaultPageSize, sorter = {}, filters = {} } =
    params && params[0] ? params[0] : ({} as any);

  // 只改变 pagination,其他参数原样传递
  const runChangePaination = useCallback(
    (paginationParams: any) => {
      const [oldPaginationParams, ...restParams] = params;
      run(
        {
          ...oldPaginationParams,
          ...paginationParams,
        },
        ...restParams,
      );
    },
    [run, params],
  );

  const total = data?.total || 0;
  const totalPage = useMemo(() => Math.ceil(total / pageSize), [pageSize, total]);

  const onChange = useCallback(
    (c: number, p: number) => {
      /// 控制页码和分页数在正确的范围内
      let toCurrent = c <= 0 ? 1 : c;
      const toPageSize = p <= 0 ? 1 : p;

      const tempTotalPage = Math.ceil(total / toPageSize);
      if (toCurrent > tempTotalPage) {
        toCurrent = tempTotalPage;
      }
      
      /// 使用处理完的,安全的数据进行翻页操作,如果处理之后的 c p 没有变动,也会发请求
      runChangePaination({
        current: c,
        pageSize: p,
      });
    },
    [total, runChangePaination],
  );
  
  /// 这个和下面那个都是基于 onChange 方法实现的
  const changeCurrent = useCallback(
    (c: number) => {
      onChange(c, pageSize);
    },
    [onChange, pageSize],
  );

  const changePageSize = useCallback(
    (p: number) => {
      onChange(current, p);
    },
    [onChange, current],
  );

  const changeCurrentRef = useRef(changeCurrent);
  changeCurrentRef.current = changeCurrent;
  /* 分页场景下,如果 refreshDeps 变化,重置分页 */
  useUpdateEffect(() => {
    /* 只有自动执行的场景, refreshDeps 才有效 */
    if (!options.manual) {
      changeCurrentRef.current(1);
    }
  }, [...refreshDeps]);

  // 表格翻页 排序 筛选等
  const changeTable = useCallback(
    (p: PaginationConfig, f?: Filter, s?: Sorter) => {
      runChangePaination({
        current: p.current,
        pageSize: p.pageSize || defaultPageSize,
        filters: f,
        sorter: s,
      });
    },
    [filters, sorter, runChangePaination],
  );

  return {
    loading,
    data,
    params,
    run,
    pagination: {
      current,
      pageSize,
      total,
      totalPage,
      onChange,
      changeCurrent,
      changePageSize,
    },
    /// 将相关的方法/属性,以 props 的方式直接返回,然后可以很方便的给 antd 的 table 使用,这一点很赞
    tableProps: {
      dataSource: data?.list || [],
      loading,
      onChange: changeTable,
      pagination: {
        current,
        pageSize,
        total,
      },
    },
    sorter,
    filters,
    ...rest,
  } as PaginatedResult<U>;
}

export default usePaginated;

复制代码

index

暴露出来的 useRequest hook,通过增加配置项的方式,整合 useAsync 、useLoadMore 、usePaginated 三种hook

/* eslint-disable react-hooks/rules-of-hooks */
import { useRef, useContext } from 'react';
import {
  BaseOptions,
  BasePaginatedOptions,
  BaseResult,
  CombineService,
  LoadMoreFormatReturn,
  LoadMoreOptions,
  LoadMoreOptionsWithFormat,
  LoadMoreParams,
  LoadMoreResult,
  OptionsWithFormat,
  PaginatedFormatReturn,
  PaginatedOptionsWithFormat,
  PaginatedParams,
  PaginatedResult,
} from './types';
import useAsync from './useAsync';
import useLoadMore from './useLoadMore';
import usePaginated from './usePaginated';
import ConfigContext from './configContext';

function useRequest<R = any, P extends any[] = any, U = any, UU extends U = any>(
  service: CombineService<R, P>,
  options: OptionsWithFormat<R, P, U, UU>,
): BaseResult<U, P>;
function useRequest<R = any, P extends any[] = any>(
  service: CombineService<R, P>,
  options?: BaseOptions<R, P>,
): BaseResult<R, P>;

function useRequest<R extends LoadMoreFormatReturn, RR>(
  service: CombineService<RR, LoadMoreParams<R>>,
  options: LoadMoreOptionsWithFormat<R, RR>,
): LoadMoreResult<R>;
function useRequest<R extends LoadMoreFormatReturn, RR extends R>(
  service: CombineService<R, LoadMoreParams<R>>,
  options: LoadMoreOptions<RR>,
): LoadMoreResult<R>;

function useRequest<R = any, Item = any, U extends Item = any>(
  service: CombineService<R, PaginatedParams>,
  options: PaginatedOptionsWithFormat<R, Item, U>,
): PaginatedResult<Item>;
function useRequest<R = any, Item = any, U extends Item = any>(
  service: CombineService<PaginatedFormatReturn<Item>, PaginatedParams>,
  options: BasePaginatedOptions<U>,
): PaginatedResult<Item>;

function useRequest(service: any, options: any = {}) {
  /// 支持使用 context 共享配置
  const contextConfig = useContext(ConfigContext);
  const finalOptions = { ...contextConfig, ...options };

  const { paginated, loadMore, requestMethod } = finalOptions;

  const paginatedRef = useRef(paginated);
  const loadMoreRef = useRef(loadMore);
  /// 保证永远只执行同一个 hook
  if (paginatedRef.current !== paginated) {
    throw Error('You should not modify the paginated of options');
  }

  if (loadMoreRef.current !== loadMore) {
    throw Error('You should not modify the loadMore of options');
  }

  paginatedRef.current = paginated;
  loadMoreRef.current = loadMore;
  
  /// 使用原生 [fetch](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch) 来发起请求
  // @ts-ignore
  const fetchProxy = (...args: any[]) =>
    // @ts-ignore
    fetch(...args).then((res: Response) => {
      if (res.ok) {
        return res.json();
      }
      throw new Error(res.statusText);
    });

  const finalRequestMethod = requestMethod || fetchProxy;

  let promiseService: () => Promise<any>;
  /// 针对传入的不同类型 service 统一处理成 promise
  /// 如果是简单的字符串,则当成路径处理
  /// 如果是对象,则当成是请求配置项处理
  /// 其他情况则当成方法来处理,然后根据返回值的不同继续处理:字符串和对象继续封装成promise,promise则直接返回,其他情况则忽略
  switch (typeof service) {
    case 'string':
      promiseService = () => finalRequestMethod(service);
      break;
    case 'object':
      const { url, ...rest } = service;
      promiseService = () => (requestMethod ? requestMethod(service) : fetchProxy(url, rest));
      break;
    default:
      promiseService = (...args: any[]) =>
        new Promise((resolve, reject) => {
          const s = service(...args);
          let fn = s;
          if (!s.then) {
            switch (typeof s) {
              case 'string':
                fn = finalRequestMethod(s);
                break;
              case 'object':
                const { url, ...rest } = s;
                fn = requestMethod ? requestMethod(s) : fetchProxy(url, rest);
                break;
            }
          }
          fn.then(resolve).catch(reject);
        });
  }
  
  /// 此次在 if 语句内部使用了 hook ,这个在 [react hook 规则](https://react.docschina.org/docs/hooks-rules.html#explanation) 中是不被推荐的
  /// 所以上面才有限制 loadMore 和 paginated 不允许被修改
  /// 这样就能保证 hook 在一个 if(true){...} 语句中执行,保证 react 的 hook 调用顺序不出问题
  if (loadMore) {
    return useLoadMore(promiseService, finalOptions);
  }
  if (paginated) {
    return usePaginated(promiseService, finalOptions);
  }
  return useAsync(promiseService, finalOptions);
}

const UseRequestProvider = ConfigContext.Provider;

// UseAPIProvider 已经废弃,此处为了兼容 umijs 插件 plugin-request
const UseAPIProvider = UseRequestProvider;

export { useAsync, usePaginated, useLoadMore, UseRequestProvider, UseAPIProvider };

export default useRequest;

复制代码

参考资料

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

分类:
前端
标签: