让我们通过解读源码偷师ahooks的hooks设计并自练习实现,来理解并掌握 React Hooks、学习优秀的代码组织和架构设计 ,在日常开发中可以多借鉴使用。
Doc: ahooks - React Hooks Library - ahooks 3.0
会把除一些特殊场景的hooks的其他hooks都过一遍
ahooks做了什么
ahooks是一套高质量可靠的 React Hooks 库。在当前 React 项目研发过程中,一套好用的 React Hooks 库是必不可少的,我们经常会在很多场景下需要使用到一些复杂的hooks,ahooks往往会成为首选,他具备以下特性
特性
- 易学易用
- 支持 SSR
- 对输入输出函数做了特殊处理,且避免闭包问题
- 包含大量提炼自业务的高级 Hooks
- 包含丰富的基础 Hooks
- 使用 TypeScript 构建,提供完整的类型定义文件
所以ahooks就是hooks集,并按照 Hooks 的功能和应用场景来进行分类和划分。主要包括以下几个类别:
- Scene Hooks: 用于处理具体场景的 Hooks。
- Effect Hooks :主要用来处理带有副作用的操作,比如
useUpdateEffect
、useTimeout
等。 - State Hooks :包含了一些管理和操作状态的 Hooks,例如
useBoolean
、useCounter
等。 - Dom Hooks :主要用于操作 DOM 元素,例如
useClickAway
、useScroll
等。 - LifeCycle Hooks :用于处理组件的生命周期,如
useMount
、useUnmount
等。 - Request Hooks :用于处理异步请求,如
useRequest
。 - Advanced Hooks:更加精细和深入,用于处理更复杂的场景。
- Dev hooks: 在开发环境下执行的hooks,比如
useWhyDidYouUpdate
下面我们也依照不同的分类来进行学习
LifeCycle Hooks
用于处理组件的生命周期
useMount
通过对useEffect指定响应式依赖项为空,实现只在组件初始化时执行fn。
php
const useMount = (fn: () => void) => {
useEffect(() => {
fn?.();
}, []);
};
useUnmount
同样设置useEffect的响应式依赖为空在组件卸载(unmount)时执行fn
ini
const useUnmount = (fn: () => void) => {
const fnRef = useLatest(fn);
useEffect(
() => () => {
fnRef.current();
},
[],
);
};
这里比较有趣的是
useLatest
,将 fn用useRef 存储,并每次渲染的时候更新fn起来避免闭包问题, 即返回的永远是最新值。
javascript
function useLatest<T>(value: T) {
const ref = useRef(value);
ref.current = value;
return ref;
}
// 使用场景
const { counter } = this.state;
// 创建一个 ref 来存储最新的 counter 值
const counterRef = useLatest(counter);
useEffect(() => {
const timerId = setInterval(() => {
console.log(counterRef.current); // 每次打印的都是最新的 counter 值
}, 1000);
return () => clearInterval(timerId);
}, [])
// 比如在其他的地方改变 counter
this.setState({ counter: counter + 1 });
对于
useLatest
,可以延伸到usePersistFn
。usePersisFn在useLatest的基础上保留了函数的引用,永远只在首次渲染时生成了一个永不改变的函数引用,做到了持久化,避免了组件更新的时候需要重新创建函数。在很多场景下都可以适当使用 usePersistFn
来替代 useCallback
,一般情况下fn可以都使用usePersistFn
进行封装。
ini
function usePersistFn<T extends noop>(fn: T) {
const fnRef = useRef<T>(fn);
fnRef.current = fn;
const persistFn = useRef<T>();
if (!persistFn.current) {
persistFn.current = function (...args) {
// @ts-ignore this
return fnRef.current.apply(this, args);
} as T;
}
return persistFn.current!;
}
useUnmountedRef
获取当前组件是否已经卸载的 Hook,利用useEffect 组件销毁时执行的实现的逻辑。
ini
const useUnmountedRef = () => {
const unmountedRef = useRef(false);
useEffect(() => {
unmountedRef.current = false;
return () => {
unmountedRef.current = true;
};
}, []);
return unmountedRef;
};
State Hooks
包含了一些管理和操作状态的 Hooks
useSetState
管理 object 类型 state 的 Hooks,用法与 class 组件的 this.setState
基本一致,增加了自动合并对象。
ini
const useSetState = <S extends Record<string, any>>(
initialState: S | (() => S),
): [S, SetState<S>] => {
const [state, setState] = useState<S>(initialState);
const setMergeState = useCallback((patch) => {
setState((prevState) => {
const newState = isFunction(patch) ? patch(prevState) : patch;
// 自动合并对象
return newState ? { ...prevState, ...newState } : prevState;
});
}, []);
return [state, setMergeState];
};
useBoolean
优雅的管理 boolean 状态,用于处理usestate需设置为boolean的情况
dart
export default function useBoolean(defaultValue = false): [boolean, Actions] {
const [state, { toggle, set }] = useToggle(!!defaultValue);
const actions: Actions = useMemo(() => {
const setTrue = () => set(true);
const setFalse = () => set(false);
return {
toggle, // 切换state状态
set: (v) => set(!!v), // 设置 state
setTrue, // 设置为 true
setFalse, // 设置为 false
};
}, []);
return [state, actions];
}
useToggle
用于在两个状态值之间切换的 Hook,如false => true
ini
function useToggle<D, R>(defaultValue: D = false as unknown as D, reverseValue?: R) {
// 初始化 state
const [state, setState] = useState<D | R>(defaultValue);
const actions = useMemo(() => {
// 计算状态相反值 false => true
const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue) as D | R;
const toggle = () => setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue));
const set = (value: D | R) => setState(value);
const setLeft = () => setState(defaultValue);
const setRight = () => setState(reverseValueOrigin);
return {
toggle, // 反转state值
set, // 设置state
setLeft, // 设置state为默认值
setRight, // 设置state为默认相反值
};
}, []);
return [state, actions];
}
useUrlState
基于 react-router
的 useLocation & useHistory & useNavigate 将 query 信息转为state进行管理。
ini
//packages/use-url-state/src/index.ts
import * as tmp from 'react-router';
const rc = tmp as any;
const useUrlState = <S extends UrlState = UrlState>(
initialState?: S | (() => S),
options?: Options,
) => {
type State = Partial<{ [key in keyof S]: any }>;
const { navigateMode = 'push', parseOptions, stringifyOptions } = options || {};
const mergedParseOptions = { ...baseParseConfig, ...parseOptions };
const mergedStringifyOptions = { ...baseStringifyConfig, ...stringifyOptions };
const location = rc.useLocation();
// react-router v5 rc export from
const history = rc.useHistory?.();
// react-router v6
const navigate = rc.useNavigate?.();
const update = useUpdate();
const initialStateRef = useRef(
typeof initialState === 'function' ? (initialState as () => S)() : initialState || {},
);
const queryFromUrl = useMemo(() => {
return qs.parse(location.search, mergedParseOptions);
}, [location.search]);
// 合并query
const targetQuery: State = useMemo(
() => ({
...initialStateRef.current,
...queryFromUrl,
}),
[queryFromUrl],
);
const setState = (s: React.SetStateAction<State>) => {
const newQuery = typeof s === 'function' ? s(targetQuery) : s;
// 1. 如果 setState 后,search 没变化,就需要 update 来触发一次更新。比如 demo1 直接点击 clear,就需要 update 来触发更新。
// 2. update 和 history 的更新会合并,不会造成多次更新
update(); //刷新一次组件
// 区分react-router v5 or v6
if (history) {
// 更新 query
history[navigateMode](
{
hash: location.hash,
search: qs.stringify({ ...queryFromUrl, ...newQuery }, mergedStringifyOptions) || '?',
},
location.state,
);
}
if (navigate) {
// 更新 query
navigate(
{
hash: location.hash,
search: qs.stringify({ ...queryFromUrl, ...newQuery }, mergedStringifyOptions) || '?',
},
{
replace: navigateMode === 'replace',
state: location.state,
},
);
}
};
return [targetQuery, useMemoizedFn(setState)] as const;
};
// useUpdate: 刷新一次组件
const useUpdate = () => {
const [, setState] = useState({});
return useCallback(() => setState({}), []);
};
useCookieState
将状态存储在 Cookie 中,并管理
ini
function useCookieState(cookieKey: string, options: Options = {}) {
const [state, setState] = useState<State>(() => {
// 读cookie 初始化state
const cookieValue = Cookies.get(cookieKey);
if (isString(cookieValue)) return cookieValue;
if (isFunction(options.defaultValue)) {
return options.defaultValue();
}
return options.defaultValue;
});
// 更新state & cookie
// useMemoizedFn 持久化fn同 usePersistFn 一样
const updateState = useMemoizedFn(
(
newValue: State | ((prevState: State) => State),
newOptions: Cookies.CookieAttributes = {},
) => {
const { defaultValue, ...restOptions } = { ...options, ...newOptions };
const value = isFunction(newValue) ? newValue(state) : newValue;
setState(value);
if (value === undefined) {
Cookies.remove(cookieKey);
} else {
Cookies.set(cookieKey, value, restOptions);
}
},
);
return [state, updateState] as const;
}
useLocalStorageState
将localStorage里的信息转为state进行管理,大致设计和cookie一样多了序列化和反序列化的处理
scss
// 判断是否为浏览器
const useLocalStorageState = createUseStorageState(() => (isBrowser ? localStorage : undefined));
export function createUseStorageState(getStorage: () => Storage | undefined) {
function useStorageState<T>(key: string, options: Options<T> = {}) {
let storage: Storage | undefined;
const {
onError = (e) => {
console.error(e);
},
} = options;
// https://github.com/alibaba/hooks/issues/800
try {
storage = getStorage();
} catch (err) {
onError(err);
}
// 序列化
const serializer = (value: T) => {
if (options.serializer) {
return options.serializer(value);
}
return JSON.stringify(value);
};
// 反序列化
const deserializer = (value: string): T => {
if (options.deserializer) {
return options.deserializer(value);
}
return JSON.parse(value);
};
// 获取store里的值
function getStoredValue() {
try {
const raw = storage?.getItem(key);
if (raw) {
return deserializer(raw);
}
} catch (e) {
onError(e);
}
if (isFunction(options.defaultValue)) {
return options.defaultValue();
}
return options.defaultValue;
}
const [state, setState] = useState(getStoredValue);
useUpdateEffect(() => {
setState(getStoredValue());
}, [key]);
// 更新state
const updateState = (value?: SetState<T>) => {
const currentState = isFunction(value) ? value(state) : value;
setState(currentState);
if (isUndef(currentState)) {
storage?.removeItem(key);
} else {
try {
storage?.setItem(key, serializer(currentState));
} catch (e) {
console.error(e);
}
}
};
return [state, useMemoizedFn(updateState)] as const;
}
return useStorageState;
}
useSessionStorageState
同useLocalStorageState一样
useDebounce
用来处理防抖
用法:
ini
const debouncedValue = useDebounce(value, { wait: 500 });
scss
function useDebounce<T>(value: T, options?: DebounceOptions) {
const [debounced, setDebounced] = useState(value);
const { run } = useDebounceFn(() => {
setDebounced(value);
}, options);
useEffect(() => {
run();
}, [value]);
return debounced;
}
// useDebounceFn
function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) {
// 获取最新fn
const fnRef = useLatest(fn);
// 执行步长
const wait = options?.wait ?? 1000;
const debounced = useMemo(
() =>
// lodash 提供的debounce(setTimeout)
debounce(
(...args: Parameters<T>): ReturnType<T> => {
return fnRef.current(...args);
},
wait,
options,
),
[],
);
// 卸载时取消debounced(clearTimeout)
useUnmount(() => {
debounced.cancel();
});
return {
run: debounced,
cancel: debounced.cancel,
flush: debounced.flush,
};
}
useThrottle
处理节流,实现原理和useDebounce一样
scss
function useThrottle<T>(value: T, options?: ThrottleOptions) {
const [throttled, setThrottled] = useState(value);
const { run } = useThrottleFn(() => {
setThrottled(value);
}, options);
useEffect(() => {
run();
}, [value]);
return throttled;
}
// useThrottleFn
function useThrottleFn<T extends noop>(fn: T, options?: ThrottleOptions) {
if (isDev) {
if (!isFunction(fn)) {
console.error(`useThrottleFn expected parameter is a function, got ${typeof fn}`);
}
}
const fnRef = useLatest(fn);
const wait = options?.wait ?? 1000;
const throttled = useMemo(
() =>
throttle(
(...args: Parameters<T>): ReturnType<T> => {
return fnRef.current(...args);
},
wait,
options,
),
[],
);
useUnmount(() => {
throttled.cancel();
});
return {
run: throttled,
cancel: throttled.cancel,
flush: throttled.flush,
};
}
useMap
管理 Map 类型状态,模拟Map的操作(setAll、remove、set、reset),并将每次修改更新到state
dart
function useMap<K, T>(initialValue?: Iterable<readonly [K, T]>) {
const getInitValue = () => new Map(initialValue);
const [map, setMap] = useState<Map<K, T>>(getInitValue);
const set = (key: K, entry: T) => {
setMap((prev) => {
const temp = new Map(prev);
temp.set(key, entry);
return temp;
});
};
const setAll = (newMap: Iterable<readonly [K, T]>) => {
setMap(new Map(newMap));
};
const remove = (key: K) => {
setMap((prev) => {
const temp = new Map(prev);
temp.delete(key);
return temp;
});
};
const reset = () => setMap(getInitValue());
const get = (key: K) => map.get(key);
return [
map,
{
set: useMemoizedFn(set),
setAll: useMemoizedFn(setAll),
remove: useMemoizedFn(remove),
reset: useMemoizedFn(reset),
get: useMemoizedFn(get),
},
] as const;
}
useSet
同useMap一样
usePrevious
保存上一次状态
ini
function usePrevious<T>(
state: T,
shouldUpdate: ShouldUpdateFunc<T> = defaultShouldUpdate,
): T | undefined {
const prevRef = useRef<T>();
const curRef = useRef<T>();
// 判断是否更新,若更新则记录更新前后的值
if (shouldUpdate(curRef.current, state)) {
prevRef.current = curRef.current;
curRef.current = state;
}
return prevRef.current;
}
useRafState
只在 requestAnimationFrame callback 时更新 state,一般用于性能优化,避免页面卡顿。可能会有以下使用场景
- 高频更新状态: 如果在 React 组件中有一个状态需要在短时间内更新很多次,对性能要求较高,例如拖拽操作、游戏、音视频播放、画布操作等,那么
useRafState
就会非常有用。 - 动画和过渡效果: 对于涉及到动画和过渡效果的场景,
useRafState
可以更平滑地控制动作,提高用户体验。 - 复杂或大数据集的交互: 在处理稍微复杂或大量的数据,并且这些数据更改可能导致频繁更新状态和重绘的情况下,
useRafState
可以优化性能,确保界面响应顺畅。
scss
function useRafState<S>(initialState?: S | (() => S)) {
// 通过ref记录AnimationFrame,卸载时清理
const ref = useRef(0);
const [state, setState] = useState(initialState);
const setRafState = useCallback((value: S | ((prevState: S) => S)) => {
cancelAnimationFrame(ref.current);
// 在requestAnimationFrame callback时才更新state
ref.current = requestAnimationFrame(() => {
setState(value);
});
}, []);
useUnmount(() => {
cancelAnimationFrame(ref.current);
});
return [state, setRafState] as const;
}
useSafeState
用法与 React.useState
完全一样,但是在组件卸载后异步回调内的 setState
不再执行,避免因组件卸载后更新状态而导致的内存泄漏。若有setState还没执行完就更新组件的情况下可以使用useSafeState。
scss
function useSafeState<S>(initialState?: S | (() => S)) {
const unmountedRef = useUnmountedRef();
const [state, setState] = useState(initialState);
const setCurrentState = useCallback((currentState) => {
/** if component is unmounted, stop update */
if (unmountedRef.current) return;
setState(currentState);
}, []);
return [state, setCurrentState] as const;
}
useGetState
为setate添加一个get函数用于获取最新的state,本质就是将每次更新通过ref存储,ref永远返回的是最新的值。useState受闭包影响,当前环境下只能获取当前的state
ini
function useGetState<S>(
initialState: S | (() => S),
): [S, Dispatch<SetStateAction<S>>, GetStateAction<S>];
function useGetState<S = undefined>(): [
S | undefined,
Dispatch<SetStateAction<S | undefined>>,
GetStateAction<S | undefined>,
];
function useGetState<S>(initialState?: S) {
const [state, setState] = useState(initialState);
const stateRef = useLatest(state);
const getState = useCallback(() => stateRef.current, []);
return [state, setState, getState];
}
useResetState
提供重置 state 方法的 Hooks
ini
const useResetState = <S>(
initialState: S | (() => S),
): [S, Dispatch<SetStateAction<S>>, ResetState] => {
const [state, setState] = useState(initialState);
// reset
const resetState = useMemoizedFn(() => {
setState(initialState);
});
return [state, setState, resetState];
};
Effect Hooks
主要用来处理带有副作用的操作
useUpdateEffect
useUpdateEffect
用法等同于 useEffect
,但是会忽略首次执行,只在依赖更新时执行。
ini
useUpdateEffect => createUpdateEffect(useEffect)
export const createUpdateEffect: (hook: EffectHookType) => EffectHookType =
(hook) => (effect, deps) => {
const isMounted = useRef(false);
// for react-refresh
hook(() => {
return () => {
isMounted.current = false;
};
}, []);
hook(() => {
// 跳过首次刷新
if (!isMounted.current) {
isMounted.current = true;
} else {
return effect();
}
}, deps);
};
useUpdateLayoutEffect
原理同useUpdateEffect一样添加isMounted(useRef)的判断,跳过updateLayoutEffect的首次刷新
useAsyncEffect
支持异步函数。useEffect是不支持异步函数的,一般都是将异步函数在useEffect函数内进行封装,比如:
javascript
useEffect(() => {
async function startFetching() {
setBio(null);
const result = await fetchBio(person);
if (!ignore) {
setBio(result);
}
}
let ignore = false;
startFetching();
return () => {
ignore = true;
}
}, [person]);
// 使用useAsyncEffect
useAsyncEffect(async () => {
async function startFetching() {
setBio(null);
const result = await fetchBio(person);
if (!ignore) {
setBio(result);
}
}
await startFetching();
// 不需要再ignore
}, [person]);
// useAsyncEffect
function useAsyncEffect(
effect: () => AsyncGenerator<void, void, void> | Promise<void>,
deps?: DependencyList,
) {
useEffect(() => {
// 执行effect
const e = effect();
let cancelled = false;
//
async function execute() {
// 是否为异步可迭代函数
if (isAsyncGenerator(e)) {
while (true) {
const result = await e.next();
if (result.done || cancelled) {
break;
}
}
} else {
await e;
}
}
execute();
return () => {
cancelled = true;
};
}, deps);
}
可见useAsyncEffect其实就是封装了异步函数,并处理了时序竞争的问题( 你的代码不会受到"竞争条件"的影响:网络响应可能会以与你发送的不同的顺序到达)。
Tips: 如果判断一个函数为异步可迭代函数
javascript
typeof fn[Symbol.asyncIterator] === 'function'
useDebounceEffect
为 useEffect
增加防抖的能力。将useEffect用useDebounceFn封装了,flag更新后才触发真的effect
scss
function useDebounceEffect(
effect: EffectCallback,
deps?: DependencyList,
options?: DebounceOptions,
) {
const [flag, setFlag] = useState({});
const { run } = useDebounceFn(() => {
setFlag({});
}, options);
useEffect(() => {
return run();
}, deps);
useUpdateEffect(effect, [flag]);
}
useThrottleEffect
原理和useDebounceEffect一样
useDeepCompareEffect
deps 通过 react-fast-compare 进行深比较。
ini
useEffect(() => {
effectCountRef.current += 1;
}, [{}]);
useDeepCompareEffect(() => {
deepCompareCountRef.current += 1;
return () => {
// do something
};
}, [{}]);
useDeepCompareEffect => createDeepCompareEffect(useEffect)
export const createDeepCompareEffect: CreateUpdateEffect = (hook) => (effect, deps) => {
const ref = useRef<DependencyList>();
const signalRef = useRef<number>(0);
// 深比较,不一致才更改signalRef。触发effect
if (deps === undefined || !depsEqual(deps, ref.current)) {
ref.current = deps;
signalRef.current += 1;
}
hook(effect, [signalRef.current]);
};
useDeepCompareLayoutEffect
原理与useDeepCompareEffect一样
useInterval
可以处理 setInterval 的 Hook
scss
// 用例
const [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000);
// useInterval
const useInterval = (fn: () => void, delay?: number, options: { immediate?: boolean } = {}) => {
const timerCallback = useMemoizedFn(fn);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const clear = useCallback(() => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
}, []);
useEffect(() => {
if (!isNumber(delay) || delay < 0) {
return;
}
if (options.immediate) {
// 执行callback
timerCallback();
}
timerRef.current = setInterval(timerCallback, delay);
// 清除Interval
return clear;
}, [delay, options.immediate]);
return clear;
};
useRafInterval
用 requestAnimationFrame
模拟实现 setInterval
,API 和 useInterval
保持一致,好处是可以在页面不渲染的时候停止执行定时器,比如页面隐藏或最小化等。
useTimeout
可以处理 setTimeout 计时器函数的 Hook。实现方式同useInterval一样
useRafTimeout
用 requestAnimationFrame
模拟实现 useTimeout,实现原理和useRafInterval
一样
useLockFn
用于给一个异步函数增加竞态锁,防止并发执行。
ini
// 通过 lockRef 标记是否已执行
function useLockFn<P extends any[] = any[], V = any>(fn: (...args: P) => Promise<V>) {
const lockRef = useRef(false);
return useCallback(
async (...args: P) => {
if (lockRef.current) return;
lockRef.current = true;
try {
const ret = await fn(...args);
return ret;
} catch (e) {
throw e;
} finally {
lockRef.current = false;
}
},
[fn],
);
}
useUpdate
useUpdate 会返回一个函数,调用该函数会强制组件重新渲染。
scss
const useUpdate = () => {
const [, setState] = useState({});
return useCallback(() => setState({}), []);
};
Dom Hooks
主要用于操作 DOM 元素
步骤
- 获取目标element
- 绑定hooks
每一个Dom hooks都需要使用到
useEffectWithTarget
去获取目标elements通过DOM 节点或者 ref后执行对应elements的effect
useEffectWithTarget🌟
ini
const useEffectWithTarget = createEffectWithTarget(useEffect);
const createEffectWithTarget = (useEffectType: typeof useEffect | typeof useLayoutEffect) => {
/**
* @param effect
* @param deps
* @param target target should compare ref.current vs ref.current, dom vs dom, ()=>dom vs ()=>dom
*/
const useEffectWithTarget = (
effect: EffectCallback,
deps: DependencyList,
target: BasicTarget<any> | BasicTarget<any>[],
) => {
// 是否初始化
const hasInitRef = useRef(false);
// 最后操作的元素
const lastElementRef = useRef<(Element | null)[]>([]);
// 依赖
const lastDepsRef = useRef<DependencyList>([]);
// 退出
const unLoadRef = useRef<any>();
useEffectType(() => {
// 判断传入的目标 DOM 节点或者 ref
const targets = Array.isArray(target) ? target : [target];
// 获取Dom elements
const els = targets.map((item) => getTargetElement(item));
// init run
if (!hasInitRef.current) {
hasInitRef.current = true;
lastElementRef.current = els;
lastDepsRef.current = deps;
unLoadRef.current = effect();
return;
}
if (
els.length !== lastElementRef.current.length ||
!depsAreSame(lastElementRef.current, els) ||
!depsAreSame(lastDepsRef.current, deps)
) {
unLoadRef.current?.();
lastElementRef.current = els;
lastDepsRef.current = deps;
unLoadRef.current = effect();
}
});
useUnmount(() => {
unLoadRef.current?.();
// for react-refresh
hasInitRef.current = false;
});
};
return useEffectWithTarget;
};
// getTargetElement
export function getTargetElement<T extends TargetType>(target: BasicTarget<T>, defaultElement?: T) {
if (!isBrowser) {
return undefined;
}
if (!target) {
return defaultElement;
}
let targetElement: TargetValue<T>;
if (isFunction(target)) {
targetElement = target();
} else if ('current' in target) {
// ref
targetElement = target.current;
} else {
// dom
targetElement = target;
}
return targetElement;
}
useEventListener
优雅的使用addEventListener
scss
function useEventListener(eventName: string, handler: noop, options: Options = {}) {
// 获取最新的handler fn
const handlerRef = useLatest(handler);
useEffectWithTarget(
() => {
const targetElement = getTargetElement(options.target, window);
if (!targetElement?.addEventListener) {
return;
}
// 处理事件
const eventListener = (event: Event) => {
return handlerRef.current(event);
};
// 添加监听事件
targetElement.addEventListener(eventName, eventListener, {
capture: options.capture,
once: options.once,
passive: options.passive,
});
// clear
return () => {
targetElement.removeEventListener(eventName, eventListener, {
capture: options.capture,
});
};
},
[eventName, options.capture, options.once, options.passive],
options.target, // DOM 节点或者 ref
);
}
useHover
监听 DOM 元素是否有鼠标悬停。
scss
// 用法
const isHovering = useHover(ref);
// useHover
export default (target: BasicTarget, options?: Options): boolean => {
const { onEnter, onLeave, onChange } = options || {};
const [state, { setTrue, setFalse }] = useBoolean(false);
useEventListener(
'mouseenter',
() => {
onEnter?.();
setTrue();
onChange?.(true);
},
{
target,
},
);
useEventListener(
'mouseleave',
() => {
onLeave?.();
setFalse();
onChange?.(false);
},
{
target,
},
);
return state;
};
其他的Hooks实现都大同小异就不再继续赘述
Advanced Hooks🌟
更加精细和深入的hooks,用于处理更复杂的场景。
useControllableValue
在某些组件开发时,我们需要组件的状态既可以自己管理,也可以被外部控制,useControllableValue
就是帮你管理这种状态的 Hook。
用例
typescript
const ControllableComponent = (props: any) => {
// 由父级控制 state
const [state, setState] = useControllableValue<string>(props);
return <input value={state} onChange={(e) => setState(e.target.value)} />;
};
ini
function useControllableValue<T = any>(props: Props = {}, options: Options<T> = {}) {
const {
defaultValue,
defaultValuePropName = 'defaultValue',
valuePropName = 'value',
trigger = 'onChange',
} = options;
const value = props[valuePropName] as T;
// 是否prop中存在value
// props 中没有 value,则组件内部自己管理 state
const isControlled = props.hasOwnProperty(valuePropName);
const initialValue = useMemo(() => {
if (isControlled) {
// 使用props的value init
return value;
}
if (props.hasOwnProperty(defaultValuePropName)) {
return props[defaultValuePropName];
}
return defaultValue;
}, []);
const stateRef = useRef(initialValue);
if (isControlled) {
stateRef.current = value;
}
const update = useUpdate();
function setState(v: SetStateAction<T>, ...args: any[]) {
// 新的state
const r = isFunction(v) ? v(stateRef.current) : v;
if (!isControlled) {
stateRef.current = r;
// 非props控制组件,需要每次setstate的时候刷新组件(模拟useState)
update();
}
if (props[trigger]) {
// 执行prop的seState 触发更新
props[trigger](r, ...args);
}
}
return [stateRef.current, useMemoizedFn(setState)] as const;
}
useCreation
对比useRef
有更好的性能,不会出现每次重渲染都初始化useRef -- React 中文文档。- 能保证memo了不会被重新计算
用法
scss
const foo = useCreation(() => new Foo(), []);
javascript
// 本质还是useref,但加上了一个模拟deps并每次重渲染的时候diff更新ref
export default function useCreation<T>(factory: () => T, deps: DependencyList) {
const { current } = useRef({
deps,
obj: undefined as undefined | T,
initialized: false,
});
// 判断是否更新ref
if (current.initialized === false || !depsAreSame(current.deps, deps)) {
current.deps = deps;
current.obj = factory();
current.initialized = true;
}
return current.obj as T;
}
// 深比较
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;
}
useEventEmitter
提供多个组件之间的事件通知(发布-订阅者)
用法
javascript
const event$ = useEventEmitter();
// emit
event$.emit('hello');
// Subscription
event$.useSubscription(val => {
console.log(val);
});
ini
export class EventEmitter<T> {
private subscriptions = new Set<Subscription<T>>();
// 给所有的订阅者发送消息
emit = (val: T) => {
for (const subscription of this.subscriptions) {
subscription(val);
}
};
useSubscription = (callback: Subscription<T>) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const callbackRef = useRef<Subscription<T>>();
callbackRef.current = callback;
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
function subscription(val: T) {
if (callbackRef.current) {
callbackRef.current(val);
}
}
// 添加订阅者
this.subscriptions.add(subscription);
return () => {
// 移除订阅者
this.subscriptions.delete(subscription);
};
}, []);
};
}
export default function useEventEmitter<T = void>() {
const ref = useRef<EventEmitter<T>>();
if (!ref.current) {
// 创建event实例
ref.current = new EventEmitter();
}
return ref.current;
}
useIsomorphicLayoutEffect
在 SSR 模式下,使用 useLayoutEffect 时会出现告警,所以在非浏览器环境下使用useEffectd代替useLayoutEffect
ini
const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect;
useLatest
返回当前最新值的 Hook,可以避免闭包问题。本质就是每次重渲染的时候都更新用于存储state都ref
csharp
// 用法
const [count, setCount] = useState(0);
const latestCountRef = useLatest(count);
// useLatest
function useLatest<T>(value: T) {
const ref = useRef(value);
// 每次重渲染都执行
ref.current = value;
return ref;
}
useMemoizedFn
持久化 function 的 Hook,一般情况下,可以使用 useMemoizedFn 完全代替 useCallback,也都可以把fn通过useMemoizdFn封装
javascript
// 用法'
const [state, setState] = useState('');
// func 地址永远不会变化
const func = useMemoizedFn(() => {
console.log(state);
})
// useMemoizedFn
function useMemoizedFn<T extends noop>(fn: T) {
const fnRef = useRef<T>(fn);
// why not write `fnRef.current = fn`?
// https://github.com/alibaba/hooks/issues/728
// 每次重渲染时更新fnRef,因为memoizedFn.current不会再更新,所以需要每次都更新fnRef
fnRef.current = useMemo<T>(() => fn, [fn]);
// 存储fn的函数地址,保障永远不变
const memoizedFn = useRef<PickFunction<T>>();
if (!memoizedFn.current) {
memoizedFn.current = function (this, ...args) {
return fnRef.current.apply(this, args);
};
}
return memoizedFn.current as T;
}
export default useMemoizedFn;
useReactive
提供一种数据响应式的操作体验,定义数据状态不需要写useState
,直接修改属性即可刷新视图(vue体验感)。
用法
javascript
const state = useReactive({
count: 0,
inputVal: '',
obj: {
value: '',
},
});
return (
<div>
<p> state.count:{state.count}</p>
<button style={{ marginRight: 8 }} onClick={() => state.count++}>
</div>
)
实现逻辑:通过代理value的修改,隐性插入useRef记录数据,在每次数据变更的时候都强制进行组件刷新
typescript
// k:v 原对象:代理过的对象
const proxyMap = new WeakMap();
// k:v 代理过的对象:原对象
const rawMap = new WeakMap();
function useReactive<S extends Record<string, any>>(initialState: S): S {
// 初始化时刷新组件
const update = useUpdate();
const stateRef = useRef<S>(initialState);
const state = useCreation(() => {
return observer(stateRef.current, () => {
update();
});
}, []);
// 通过proxy代理每次value修改,触发cb(update)
function observer<T extends Record<string, any>>(initialVal: T, cb: () => void): T {
const existingProxy = proxyMap.get(initialVal);
// 添加缓存 防止重新构建proxy
if (existingProxy) {
return existingProxy;
}
// 防止代理已经代理过的对象
// https://github.com/alibaba/hooks/issues/839
if (rawMap.has(initialVal)) {
return initialVal;
}
const proxy = new Proxy<T>(initialVal, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
// https://github.com/alibaba/hooks/issues/1317
const descriptor = Reflect.getOwnPropertyDescriptor(target, key);
if (!descriptor?.configurable && !descriptor?.writable) {
return res;
}
// Only proxy plain object or array,
// otherwise it will cause: https://github.com/alibaba/hooks/issues/2080
return isPlainObject(res) || Array.isArray(res) ? observer(res, cb) : res;
},
set(target, key, val) {
const ret = Reflect.set(target, key, val);
cb();
return ret;
},
deleteProperty(target, key) {
const ret = Reflect.deleteProperty(target, key);
cb();
return ret;
},
});
proxyMap.set(initialVal, proxy);
rawMap.set(proxy, initialVal);
return proxy;
}
return state;
}
Dev Hooks
在开发环境下执行的hooks
useTrackedEffect
追踪是哪个依赖变化触发了 useEffect
的执行。
原理:用useRef记录每次修改后的dep,与下次dep进行diff判断哪些依赖变更了
ini
const diffTwoDeps = (deps1?: DependencyList, deps2?: DependencyList) => {
return deps1
? deps1
.map((_ele, idx) => (!Object.is(deps1[idx], deps2?.[idx]) ? idx : -1))
.filter((ele) => ele >= 0)
: deps2
? deps2.map((_ele, idx) => idx)
: [];
};
const useTrackedEffect = <T extends DependencyList>(effect: Effect<T>, deps?: [...T]) => {
// 记录每次修改后的dep,与下次dep进行diff判断哪些依赖变更了
const previousDepsRef = useRef<T>();
useEffect(() => {
const changes = diffTwoDeps(previousDepsRef.current, deps);
const previousDeps = previousDepsRef.current;
previousDepsRef.current = deps;
return effect(changes, previousDeps, deps);
}, deps);
};
useWhyDidYouUpdate
帮助开发者排查是哪个属性改变导致了组件的 rerender
原理和useTrackedEffect一样
ini
export default function useWhyDidYouUpdate(componentName: string, props: IProps) {
// 记录props
const prevProps = useRef<IProps>({});
useEffect(() => {
if (prevProps.current) {
const allKeys = Object.keys({ ...prevProps.current, ...props });
const changedProps: IProps = {};
allKeys.forEach((key) => {
if (!Object.is(prevProps.current[key], props[key])) {
changedProps[key] = {
from: prevProps.current[key],
to: props[key],
};
}
});
if (Object.keys(changedProps).length) {
console.log('[why-did-you-update]', componentName, changedProps);
}
}
prevProps.current = props;
});
}
useRequest🌟
useRequest
是一个强大的异步数据管理的 Hooks,通过插件式组织代码,其核心代码极其简单,并且可以很方便的扩展出更高级的功能。目前已有能力包括:
- 自动请求/手动请求
- 轮询
- 防抖
- 节流
- 屏幕聚焦重新请求
- 错误重试
- loading delay
- SWR(stale-while-revalidate)
- 缓存
我们逐步解析这些能力的源码实现
自动请求/手动请求
用例
arduino
// 自动请求
const { data, error, loading } = useRequest(getUsername);
// 手动请求
const { loading, run } = useRequest(changeUsername, {
manual: true
});
源码分析
scss
function useRequest<TData, TParams extends any[]>(
service: Service<TData, TParams>, // 需要执行的函数
options?: Options<TData, TParams>, // 可配置参数
plugins?: Plugin<TData, TParams>[], // 插件
) {
return useRequestImplement<TData, TParams>(
service,
options,
[
...(plugins || []),
useDebouncePlugin,
useLoadingDelayPlugin,
usePollingPlugin,
useRefreshOnWindowFocusPlugin,
useThrottlePlugin,
useAutoRunPlugin,
useCachePlugin,
useRetryPlugin,
] as Plugin<TData, TParams>[]);
}
// useRequestImplement
function useRequestImplement<TData, TParams extends any[]>(
service: Service<TData, TParams>,
options: Options<TData, TParams> = {},
plugins: Plugin<TData, TParams>[] = [],
) {
const { manual = false, ...rest } = options;
// options
const fetchOptions = {
manual,
...rest,
};
// 获取最新的service fn
const serviceRef = useLatest(service);
// 首次需要强制刷新,初始化默认值
const update = useUpdate();
// 利用useCreation创建fetch 实例,仅在第一次更新
const fetchInstance = useCreation(() => {
// 执行插件
const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
// 发起请求
return new Fetch<TData, TParams>(
serviceRef,
fetchOptions,
update,
Object.assign({}, ...initState),
);
}, []);
fetchInstance.options = fetchOptions;
// run all plugins hooks
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
useMount(() => {
if (!manual) {
// 不是手动触发,首次自动触发
// useCachePlugin can set fetchInstance.state.params from cache when init
const params = fetchInstance.state.params || options.defaultParams || [];
// @ts-ignore
fetchInstance.run(...params);
}
});
useUnmount(() => {
// 取消请求
fetchInstance.cancel();
});
// 返回fetch状态
return {
loading: fetchInstance.state.loading,
data: fetchInstance.state.data,
error: fetchInstance.state.error,
params: fetchInstance.state.params || [],
cancel: useMemoizedFn(fetchInstance.cancel.bind(fetchInstance)),
refresh: useMemoizedFn(fetchInstance.refresh.bind(fetchInstance)),
refreshAsync: useMemoizedFn(fetchInstance.refreshAsync.bind(fetchInstance)),
run: useMemoizedFn(fetchInstance.run.bind(fetchInstance)),
runAsync: useMemoizedFn(fetchInstance.runAsync.bind(fetchInstance)),
mutate: useMemoizedFn(fetchInstance.mutate.bind(fetchInstance)),
} as Result<TData, TParams>;
}
从这可以了解到Fetch作为useRequest的核心承接了请求的发起、状态管理、取消等能力
Fetch
kotlin
// packages/hooks/src/useRequest/src/Fetch.ts
export default class Fetch<TData, TParams extends any[]> {
pluginImpls: PluginReturn<TData, TParams>[];
// 记录请求是否正常执行完成
count: number = 0;
state: FetchState<TData, TParams> = {
loading: false,
params: undefined,
data: undefined,
error: undefined,
};
constructor(
public serviceRef: MutableRefObject<Service<TData, TParams>>,
public options: Options<TData, TParams>,
public subscribe: Subscribe,
public initState: Partial<FetchState<TData, TParams>> = {},
) {
// 初始化
this.state = {
...this.state,
loading: !options.manual,
...initState,
};
}
// 更新state
setState(s: Partial<FetchState<TData, TParams>> = {}) {
this.state = {
...this.state,
...s,
};
// update(),强制刷新组件
this.subscribe();
}
// 执行插件(实现轮询、ready、防抖等)
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
// @ts-ignore
const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);
return Object.assign({}, ...r);
}
// 处理异步run函数
async runAsync(...params: TParams): Promise<TData> {
this.count += 1;
const currentCount = this.count;
const {
stopNow = false,
returnNow = false,
...state
} = this.runPluginHandler('onBefore', params);
// stop request
if (stopNow) {
return new Promise(() => {});
}
this.setState({
loading: true,
params,
...state,
});
// return now
if (returnNow) {
return Promise.resolve(state.data);
}
this.options.onBefore?.(params);
try {
// replace service
let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);
if (!servicePromise) {
servicePromise = this.serviceRef.current(...params);
}
// 执行service
const res = await servicePromise;
if (currentCount !== this.count) {
// prevent run.then when request is canceled
return new Promise(() => {});
}
this.setState({
data: res,
error: undefined,
loading: false,
});
// 执行对于的hooks cb
this.options.onSuccess?.(res, params);
this.runPluginHandler('onSuccess', res, params);
this.options.onFinally?.(params, res, undefined);
if (currentCount === this.count) {
this.runPluginHandler('onFinally', params, res, undefined);
}
return res;
} catch (error) {
if (currentCount !== this.count) {
// prevent run.then when request is canceled
return new Promise(() => {});
}
this.setState({
error,
loading: false,
});
this.options.onError?.(error, params);
this.runPluginHandler('onError', error, params);
this.options.onFinally?.(params, undefined, error);
if (currentCount === this.count) {
this.runPluginHandler('onFinally', params, undefined, error);
}
throw error;
}
}
// 暴露run函数
run(...params: TParams) {
this.runAsync(...params).catch((error) => {
if (!this.options.onError) {
console.error(error);
}
});
}
cancel() {
this.count += 1;
this.setState({
loading: false,
});
this.runPluginHandler('onCancel');
}
refresh() {
// @ts-ignore
this.run(...(this.state.params || []));
}
refreshAsync() {
// @ts-ignore
return this.runAsync(...(this.state.params || []));
}
mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
const targetData = isFunction(data) ? data(this.state.data) : data;
this.runPluginHandler('onMutate', targetData);
this.setState({
data: targetData,
});
}
}
到这儿整个逻辑都清晰啦
-
useRequestImplement
初始化fetch(注入参数和插件),并处理mount和unmount
-
Fetch 收集请求整个过程的state,并在每次state更新的时候都通过useUpdate()强制刷新组件更新页面
- 在对应阶段(sucessed or failed)触发plugins hooks
-
通过插件提供其他的能力,包括轮询、Ready等
接着我们继续看一些关键的能力
轮询(usePollingPlugin)
通过设置 options.pollingInterval
,进入轮询模式,useRequest
会定时触发 service 执行。
通过在请求onfinish的时候setTimeout延时执行(不断递归调用),并在外层记录轮询次数和时间
ini
const { data, run, cancel } = useRequest(getUsername, {
pollingInterval: 3000,
});
// usePollingPlugin
const usePollingPlugin: Plugin<any, any[]> = (
fetchInstance,
{ pollingInterval, pollingWhenHidden = true, pollingErrorRetryCount = -1 },
) => {
// 记录时间
const timerRef = useRef<Timeout>();
const unsubscribeRef = useRef<() => void>();
// 记录轮询次数
const countRef = useRef<number>(0);
// 停止轮询
const stopPolling = () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
unsubscribeRef.current?.();
};
// 到时间停止轮询
useUpdateEffect(() => {
if (!pollingInterval) {
stopPolling();
}
}, [pollingInterval]);
if (!pollingInterval) {
return {};
}
return {
onBefore: () => {
stopPolling();
},
// 记录轮询次数
onError: () => {
countRef.current += 1;
},
onSuccess: () => {
countRef.current = 0;
},
onFinally: () => {
if (
pollingErrorRetryCount === -1 ||
// 当发生错误时,在轮询错误重试次数后,请求不会重复
(pollingErrorRetryCount !== -1 && countRef.current <= pollingErrorRetryCount)
) {
timerRef.current = setTimeout(() => {
// 通过setTimeout进行轮询,
if (!pollingWhenHidden && !isDocumentVisible()) {
unsubscribeRef.current = subscribeReVisible(() => {
fetchInstance.refresh();
});
} else {
fetchInstance.refresh();
}
}, pollingInterval);
} else {
countRef.current = 0;
}
},
onCancel: () => {
stopPolling();
},
};
};
防抖(useDebouncePlugin)
通过debounce封装runAsync后注入回fetchInstance
ini
const useDebouncePlugin: Plugin<any, any[]> = (
fetchInstance,
{ debounceWait, debounceLeading, debounceTrailing, debounceMaxWait },
) => {
// 记录 debounced 封装的函数
const debouncedRef = useRef<DebouncedFunc<any>>();
// init options
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) {
// 从fetchInstance.runAsync创建 runAsync 函数
const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
// 创建基于options防抖执行的函数
debouncedRef.current = debounce(
(callback) => {
callback();
},
debounceWait,
options,
);
// debounce runAsync should be promise
// https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
// 使用 debounce 与返回 Promise 的函数一起使用,在第一次调用时不会返回 Promise。
// 所以使用promise再包一层
fetchInstance.runAsync = (...args) => {
return new Promise((resolve, reject) => {
debouncedRef.current?.(() => {
_originRunAsync(...args)
.then(resolve)
.catch(reject);
});
});
};
return () => {
debouncedRef.current?.cancel();
// 修改fetchInstance.runAsync,保障其他的plugin执行run的时候仍是debounce处理过的
fetchInstance.runAsync = _originRunAsync;
};
}
}, [debounceWait, options]);
if (!debounceWait) {
return {};
}
return {
onCancel: () => {
debouncedRef.current?.cancel();
},
};
};
由此可见plugin的执行顺序也是至关重要的
节流
实现逻辑和防抖一样,就是将debounce改为了throttle。都来自于 lodash
缓存 & SWR
"SWR" 这个名字来自于
stale-while-revalidate
:一种由 HTTP RFC 5861(opens in a new tab) 推广的 HTTP 缓存失效策略。这种策略首先从缓存中返回数据(过期的),同时发送 fetch 请求(重新验证),最后得到最新数据。
-
通过cache存储缓存的数据
- 若有缓存数据,不继续请求,使用缓存数据
-
基于发布订阅者设计,在下次组件初始化时,如果有缓存数据,通知fetchInstance 初始化state返回缓存数据,再发起请求。实现SWR
- 本质用一个数组把需要SWR的fetchInstance 都存起来了
- 然后在每次组件重渲染的时候遍历这个数组,从cache里读出数据设置每个fetchInstance的state
无论是cache还是subscribed都是全局变量,里面存储的整个页面的fetch数据
ini
// 存储需要缓存的数据
const cache = new Map<CachedKey, RecordData>();
const setCache = (key: CachedKey, cacheTime: number, cachedData: CachedData) => {
const currentCache = cache.get(key);
if (currentCache?.timer) {
clearTimeout(currentCache.timer);
}
let timer: Timer | undefined = undefined;
if (cacheTime > -1) {
// if cache out, clear it
timer = setTimeout(() => {
cache.delete(key);
}, cacheTime);
}
cache.set(key, {
...cachedData,
timer,
});
};
const getCache = (key: CachedKey) => {
return cache.get(key);
};
const clearCache = (key?: string | string[]) => {
if (key) {
const cacheKeys = Array.isArray(key) ? key : [key];
cacheKeys.forEach((cacheKey) => cache.delete(cacheKey));
} else {
cache.clear();
}
};
const useCachePlugin: Plugin<any, any[]> = (
fetchInstance,
{
cacheKey,
cacheTime = 5 * 60 * 1000,
staleTime = 0,
setCache: customSetCache,
getCache: customGetCache,
},
) => {
const unSubscribeRef = useRef<() => void>();
const currentPromiseRef = useRef<Promise<any>>();
// 自定义缓存比如存储到localStorage
const _setCache = (key: string, cachedData: CachedData) => {
if (customSetCache) {
customSetCache(cachedData);
} else {
setCache(key, cacheTime, cachedData);
}
// 发布订阅者 触发每一个订阅者 更新fetchInstance的state
trigger(key, cachedData.data);
};
const _getCache = (key: string, params: any[] = []) => {
if (customGetCache) {
return customGetCache(params);
}
return getCache(key);
};
// 仅第一次自动执行
useCreation(() => {
if (!cacheKey) {
return;
}
// get data from cache when init
const cacheData = _getCache(cacheKey);
if (cacheData && Object.hasOwnProperty.call(cacheData, 'data')) {
fetchInstance.state.data = cacheData.data;
fetchInstance.state.params = cacheData.params;
if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
fetchInstance.state.loading = false;
}
}
// 通知fetchInstance 初始化state (swr)
// subscribe same cachekey update, trigger update
unSubscribeRef.current = 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,
error: undefined,
returnNow: true,
};
} else {
// If the data is stale, return data, and request continue
return {
data: cacheData?.data,
error: undefined,
};
}
},
onRequest: (service, args) => {
let servicePromise = getCachePromise(cacheKey);
// 如果有servicePromise,并且不是由自身触发,则使用它
if (servicePromise && servicePromise !== currentPromiseRef.current) {
return { servicePromise };
}
servicePromise = service(...args);
currentPromiseRef.current = servicePromise;
setCachePromise(cacheKey, servicePromise);
return { servicePromise };
},
onSuccess: (data, params) => {
if (cacheKey) {
// cancel subscribe, avoid trgger self
// 订阅者出对列,清除需要设置state的fetchInstance
unSubscribeRef.current?.();
// 设置缓存值
_setCache(cacheKey, {
data,
params,
time: new Date().getTime(),
});
// resubscribe
// 订阅者入对列
unSubscribeRef.current = subscribe(cacheKey, (d) => {
fetchInstance.setState({ data: d });
});
}
},
// 直接修改 data的时候也更新缓存
onMutate: (data) => {
if (cacheKey) {
// cancel subscribe, avoid trigger self
unSubscribeRef.current?.();
_setCache(cacheKey, {
data,
params: fetchInstance.state.params,
time: new Date().getTimo xme(),
});
// resubscribe
unSubscribeRef.current = subscribe(cacheKey, (d) => {
fetchInstance.setState({ data: d });
});
}
},
};
};
总结
以上我们把ahooks的hook全部都过了一遍,并且是一行行代码看,增进了在日常开发中对react的使用理解,对于不同的渲染场景我们也知道了该使用什么方法来解决,但也别搭建过多的hook导致代码阅读变复杂~~