上一篇我们阅读了useRequest 的核心代码,这一篇我们接着来看看每个插件都是在怎么实现的。
首先我们来看看plugin的TS,以方便理解后面的代码。
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>>; // 返回插件初始化值
};
接下来我们来看看具体有哪些插件。
useAutoRunPlugin
这个插件主要做了什么呢。
-
判断是否自动触发请求;
-
在ready 为false 的时候,阻止触发请求;
-
当refreshDeps 更改的时候自动刷新;
-
值的注意一点的是,我们可以自己设置触发的回调,这个没有在文档中显示。如果没有设置,则直接调用 refresh 刷新。
const useAutoRunPlugin: Plugin<any, any[]> = ( fetchInstance, { manual, ready = true, defaultParams = [], refreshDeps = [], refreshDepsAction }, ) => { const hasAutoRun = useRef(false); hasAutoRun.current = false; // 是否准备好,并自动刷新
useUpdateEffect(() => { if (!manual && ready) { hasAutoRun.current = true; fetchInstance.run(...defaultParams); } }, [ready]); // 依赖变化自动刷新,也可以自己设置刷新回调函数 useUpdateEffect(() => { if (hasAutoRun.current) { return; } if (!manual) { hasAutoRun.current = true; if (refreshDepsAction) { refreshDepsAction(); } else { fetchInstance.refresh(); } } }, [...refreshDeps]);return { onBefore: () => { // 没有准备好阻止触发请求 if (!ready) { return { stopNow: true, }; } }, }; };
useDebouncePlugin 和 useThrottlePlugin
这2个差不多,底层是通过lodash 的debounce 和 throttle 来实现的,所以放在一起说,看一个就可以了。
const useDebouncePlugin: Plugin<any, any[]> = (
fetchInstance,
{ debounceWait, debounceLeading, debounceTrailing, debounceMaxWait },
) => {
const debouncedRef = useRef<DebouncedFunc<any>>();
const options = useMemo(() => {
const ret: DebounceSettings = {};
if (debounceLeading !== undefined) {
ret.leading = debounceLeading;
}
if (debounceTrailing !== undefined) {
ret.trailing = debounceTrailing;
}
if (debounceMaxWait !== undefined) {
ret.maxWait = debounceMaxWait;
}
return ret;
}, [debounceLeading, debounceTrailing, debounceMaxWait]);
useEffect(() => {
// 开启防抖
if (debounceWait) {
// 这里对原来的方法做了一次拦截
const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
debouncedRef.current = debounce(
(callback) => {
callback();
},
debounceWait,
options,
);
// debounce runAsync should be promise
// https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
fetchInstance.runAsync = (...args) => {
return new Promise((resolve, reject) => {
debouncedRef.current?.(() => {
_originRunAsync(...args)
.then(resolve)
.catch(reject);
});
});
};
return () => {
debouncedRef.current?.cancel();
// 取消拦截
fetchInstance.runAsync = _originRunAsync;
};
}
}, [debounceWait, options]);
if (!debounceWait) {
return {};
}
return {
// 取消防抖
onCancel: () => {
debouncedRef.current?.cancel();
},
};
};
useLoadingDelayPlugin
通过setTimeout延迟显示loading,避免闪烁。这个代码很简单,大家看看就能明白。
const useLoadingDelayPlugin: Plugin<any, any[]> = (fetchInstance, { loadingDelay }) => {
const timerRef = useRef<Timeout>();
if (!loadingDelay) {
return {};
}
const cancelTimeout = () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
return {
onBefore: () => {
cancelTimeout();
timerRef.current = setTimeout(() => {
fetchInstance.setState({
loading: true,
});
}, loadingDelay);
return {
loading: false,
};
},
onFinally: () => {
cancelTimeout();
},
onCancel: () => {
cancelTimeout();
},
};
};
usePollingPlugin
通过setTimeout,按pollingInterval 时间调用refresh来轮询,其中还判断了当浏览器隐藏的时候是否轮询,这里监听了浏览器的visibilitychange 事件。
const usePollingPlugin: Plugin<any, any[]> = (
fetchInstance,
{ pollingInterval, pollingWhenHidden = true },
) => {
const timerRef = useRef<Timeout>();
const unsubscribeRef = useRef<() => void>();
// 停止轮询
const stopPolling = () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
unsubscribeRef.current?.();
};
useUpdateEffect(() => {
if (!pollingInterval) {
stopPolling();
}
}, [pollingInterval]);
if (!pollingInterval) {
return {};
}
return {
onBefore: () => {
stopPolling();
},
onFinally: () => {
// 当前浏览器是隐藏状态,并且设置隐藏是不轮询,则停止轮询
if (!pollingWhenHidden && !isDocumentVisible()) {
// 订阅浏览器是否可见
unsubscribeRef.current = subscribeReVisible(() => {
fetchInstance.refresh();
});
return;
}
timerRef.current = setTimeout(() => {
fetchInstance.refresh();
}, pollingInterval);
},
onCancel: () => {
stopPolling();
},
};
};
useRefreshOnWindowFocusPlugin
当浏览器可见或是聚焦时,是否重新请求,这里监听了浏览器的visibilitychange 和focus 事件。
const useRefreshOnWindowFocusPlugin: Plugin<any, any[]> = (
fetchInstance,
{ refreshOnWindowFocus, focusTimespan = 5000 },
) => {
const unsubscribeRef = useRef<() => void>();
const stopSubscribe = () => {
unsubscribeRef.current?.();
};
useEffect(() => {
if (refreshOnWindowFocus) {
// 相当于限流
const limitRefresh = limit(fetchInstance.refresh.bind(fetchInstance), focusTimespan);
// 订阅浏览器是否可见和聚焦
unsubscribeRef.current = subscribeFocus(() => {
limitRefresh();
});
}
return () => {
stopSubscribe();
};
}, [refreshOnWindowFocus, focusTimespan]);
useUnmount(() => {
stopSubscribe();
});
return {};
};
useRetryPlugin
请求失败,通过setTimeout 重试 retryCount 次,并且可以通过 retryInverval 设置重试时间间隔,最大不超过30s.
const useRetryPlugin: Plugin<any, any[]> = (fetchInstance, { retryInterval, retryCount }) => {
const timerRef = useRef<Timeout>();
const countRef = useRef(0); // 重试次数
const triggerByRetry = useRef(false); // 是否通过重试触发请求
if (!retryCount) {
return {};
}
return {
onBefore: () => {
// 请求之前 重置重试次数,并将之前的setTimeout移除
if (!triggerByRetry.current) {
countRef.current = 0;
}
triggerByRetry.current = false;
if (timerRef.current) {
clearTimeout(timerRef.current);
}
},
onSuccess: () => {
// 成功后重置重试次数
countRef.current = 0;
},
onError: () => {
// 失败一次就增加一次重试次数
countRef.current += 1;
// 如果一直重试,或是重试次数小于配置的可重试次数,通过refresh 刷新请求
if (retryCount === -1 || countRef.current <= retryCount) {
// 如果没有设置重试间隔,则每隔 2s, 4s, 6s...递增的时间来重试刷新请求,最大间隔不大于30s
const timeout = retryInterval ?? Math.min(1000 * 2 ** countRef.current, 30000);
timerRef.current = setTimeout(() => {
triggerByRetry.current = true;
fetchInstance.refresh();
}, timeout);
} else {
// 重试次数超过配置的可重试次数,则停止重试刷新请求,并重置重试次数
countRef.current = 0;
}
},
onCancel: () => {
// cancel 后重置重试次数,并将setTimeout移除
countRef.current = 0;
if (timerRef.current) {
clearTimeout(timerRef.current);
}
},
};
};
useCachePlugin
缓存内容包括成功请求后返回的数据,请求参数和缓存时的时间。默认会使用Map 缓存数据在内存中,下次组件初始化的时候如果有缓存数据,会先使用缓存数据渲染,然后背后请求接口成功后再使用新数据渲染。
const useCachePlugin: Plugin<any, any[]> = (
fetchInstance,
{
cacheKey,
cacheTime = 5 * 60 * 1000, // 数据缓存时间,默认 5s
staleTime = 0, // 数据保持新鲜的时间,该时间内不会发送请求
setCache: customSetCache, // 自定义缓存方式,可以将数据缓存到 localStorage、IndexDB 等
getCache: customGetCache, // 通过自定义方式获取缓存数据
},
) => {
const unSubscribeRef = useRef<() => void>();
const currentPromiseRef = useRef<Promise<any>>();
// 设置缓存,2种方式,自定义的和默认的
const _setCache = (key: string, cachedData: CachedData) => {
if (customSetCache) {
customSetCache(cachedData);
} else {
cache.setCache(key, cacheTime, cachedData);
}
// 缓存的订阅
cacheSubscribe.trigger(key, cachedData.data);
};
// 获取缓存, 2种方式, 自定义和默认的
const _getCache = (key: string, params: any[] = []) => {
if (customGetCache) {
return customGetCache(params);
}
return cache.getCache(key);
};
useCreation(() => {
if (!cacheKey) {
return;
}
// 开始缓存
const cacheData = _getCache(cacheKey);
// 有缓存数据,并且缓存的数据有data值,将重置实例的state值
if (cacheData && Object.hasOwnProperty.call(cacheData, 'data')) {
fetchInstance.state.data = cacheData.data;
fetchInstance.state.params = cacheData.params;
// 如果缓存数据可用,则loading 为false, 不会再发请求
if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
fetchInstance.state.loading = false;
}
}
// 订阅相同 cacheKey 的更新,从而触发更新
unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (data) => {
fetchInstance.setState({ data });
});
}, []);
useUnmount(() => {
// 移除订阅
unSubscribeRef.current?.();
});
if (!cacheKey) {
return {};
}
return {
onBefore: (params) => {
// 请求之前获取缓存值
const cacheData = _getCache(cacheKey, params);
if (!cacheData || !Object.hasOwnProperty.call(cacheData, 'data')) {
return {};
}
// 如果数据是新鲜的,停止请求,立即返回缓存的值
if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
return {
loading: false,
data: cacheData?.data,
returnNow: true,
};
} else {
// 如果缓存可用,返回缓存的值,但是继续请求,成功返回后的值将覆盖缓存的值
return {
data: cacheData?.data,
};
}
},
onRequest: (service, args) => {
// 获取缓存的请求Promise
let servicePromise = cachePromise.getCachePromise(cacheKey);
// 如果有缓存的请求Promise 并且没有被触发过,将触发这个缓存的请求Promise获取数据,不然用传进来的请求获取数据,并缓存起来
if (servicePromise && servicePromise !== currentPromiseRef.current) {
return { servicePromise };
}
servicePromise = service(...args);
currentPromiseRef.current = servicePromise;
cachePromise.setCachePromise(cacheKey, servicePromise);
return { servicePromise };
},
onSuccess: (data, params) => {
if (cacheKey) {
// 移除订阅,避免自己触发
unSubscribeRef.current?.();
// 将成功获取的返回值缓存起来
_setCache(cacheKey, {
data,
params,
time: new Date().getTime(),
});
// 再次订阅
unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (d) => {
fetchInstance.setState({ data: d });
});
}
},
onMutate: (data) => {
// 更新数据和请求成功一样
if (cacheKey) {
// cancel subscribe, avoid trgger self
unSubscribeRef.current?.();
_setCache(cacheKey, {
data,
params: fetchInstance.state.params,
time: new Date().getTime(),
});
// resubscribe
unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (d) => {
fetchInstance.setState({ data: d });
});
}
},
};
};
看到这里,useRequest的插件我们就讲完了。
虽然官方文档没有说明可以自己扩展插件,但是从源码的角度来看是可以的。如果开发过程中需要我们自己扩展插件的话,也是非常的容易,其实主要注意2个地方,一个是通过onInit 返回插件的初始值从而重置Fetch 中的初始值,第二个就是返回插件的钩子函数,通过钩子函数,可以让我们在不同的请求生命周期中完成我们想要执行的逻辑,大家不妨试试~
utils 中的部分hooks
通过阅读源码,我发现里面有些工具hooks 也是非常好用,或是值得学习一下的,希望开发时碰到类似问题的时候,这些hooks能打开你的思路。
1.如果想强制刷新组件。
[, setState] = useState({});
() => setState({})
通过设置一个空对象,引用地址的变化,导致state变化,从而使组件重新渲染。
来源: useUpdate
2. 用useRef 创建复杂常量容易出现潜在性能问题。
const a = useRef(new Subject()) // 每次重渲染,都会执行实例化 Subject 的过程,即便这个实例立刻就被扔掉了
export default function useCreation<T>(factory: () => T, deps: DependencyList) {
const { current } = useRef({
deps,
obj: undefined as undefined | T
initialized: false,
});
if (current.initialized === false || !depsAreSame(current.deps, deps)) {
current.deps = deps;
current.obj = factory();
current.initialized = true;
}
return current.obj as T;
}
depsAreSame 怎么判断依赖是否相同呢。
export default function depsAreSame(oldDeps: DependencyList, deps: DependencyList): boolean {
if (oldDeps === deps) return true;
for (let i = 0; i < oldDeps.length; i++) {
if (!Object.is(oldDeps[i], deps[i])) return false;
}
return true;
}
来源: useCreation
3.判断文档是否可见
export const isUndef = (value: unknown): value is undefined => typeof value === 'undefined';
export default function canUseDom() {
return !!(!isUndef(window) && window.document && window.document.createElement);
}
export default function isDocumentVisible(): boolean {
if (canUseDom()) {
return document.visibilityState !== 'hidden';
}
return true;
}
来源:isDocumentVisible
4.判断浏览器是否在线
export default function isOnline(): boolean {
if (canUseDom() && !isUndef(navigator.onLine)) {
return navigator.onLine;
}
return true;
}
来源: isOnline
5.订阅屏幕聚焦变化
const listeners: any[] = [];
function subscribe(listener: () => void) {
listeners.push(listener);
return function unsubscribe() {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}
if (canUseDom()) {
const revalidate = () => {
if (!isDocumentVisible() || !isOnline()) return;
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
};
window.addEventListener('visibilitychange', revalidate, false);
window.addEventListener('focus', revalidate, false);
}
export default subscribe;
来源: subscribeFocus
使用:
// 文档可见时添加订阅
unsubscribeRef.current = subscribeFocus(() => {
someListener(); // 订阅执行的代码
});
// 停止订阅
unsubscribeRef.current?.();
到此,我们 ahooks 的 useRequest 的源码也就阅读完了,希望这2篇文章对你开发有帮助~