这个系列是将 ahooks 里面的所有 hook 源码都进行解读,通过解读 ahooks 的源码来熟悉自定义 hook 的写法,提高自己写自定义 hook 的能力,希望能够对大家有所帮助。
为了和代码原始注释区分,个人理解部分使用 ///开头,此处和 三斜线指令没有关系,只是为了做区分。
往期回顾
- ahooks 源码解读系列
- ahooks 源码解读系列 - 2
- ahooks 源码解读系列 - 3
- ahooks 源码解读系列 - 4
- ahooks 源码解读系列 - 5
- ahooks 源码解读系列 - 6
- ahooks 源码解读系列 - 7
- ahooks 源码解读系列 - 8
- ahooks 源码解读系列 - 9
- ahooks 源码解读系列 - 10
- ahooks 源码解读系列 - 11
- ahooks 源码解读系列 - 12
- ahooks 源码解读系列 - 13
- ahooks 源码解读系列 - 14
上一篇是不是看的脑阔痛,今天来点简单的~
下面让我们一起将 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;
参考资料
以上内容由于本人水平问题难免有误,欢迎大家进行讨论反馈。