这个系列是将 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
开学啦,开学啦~
终于来到了 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;
以上内容由于本人水平问题难免有误,欢迎大家进行讨论反馈。