-
本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
-
这是源码共读的第43期,链接:传送门。
-
撰写日期 2023-06-08,源码 react-use v17.4.0
react-use hooks 目录分类
Sensors
useMouse(ref)
使用说明:传入目标元素,返回当前鼠标的系列坐标(如:鼠标文档横纵轴坐标,目标元素文档横纵轴坐标,相对于目标元素横纵轴坐标…)
const useMouse = (ref: RefObject<Element>): State => {
if (process.env.NODE_ENV === 'development') {
if (typeof ref !== 'object' || typeof ref.current === 'undefined') {
console.error('useMouse expects a single ref argument.');
}
}
// useRafState 是类似 useState 的版本,但使用了 rAF (requestAnimationFrame)
// setState 时确保每60帧刷新间隙,更新 state
const [state, setState] = useRafState<State>({
docX: 0,
docY: 0,
posX: 0,
posY: 0,
elX: 0,
elY: 0,
elH: 0,
elW: 0,
});
useEffect(() => {
const moveHandler = (event: MouseEvent) => {
if (ref && ref.current) {
const { left, top, width: elW, height: elH } = ref.current.getBoundingClientRect();
// pageXOffset, window.scrollX 的别名,即横向滚动距离
// pageYOffset, window.scrollY 的别名,即纵向滚动距离
const posX = left + window.pageXOffset;
const posY = top + window.pageYOffset;
const elX = event.pageX - posX;
const elY = event.pageY - posY;
setState({
docX: event.pageX, // 鼠标事件横轴坐标(包含滚动)
docY: event.pageY, // 鼠标事件纵轴坐标(包含滚动)
posX, // ref 引用元素左上角横轴坐标(包含滚动)
posY, // ref 引用元素左上角纵轴坐标(包含滚动)
elX, // 鼠标事件相对于 ref 引用元素横轴坐标
elY, // 鼠标事件相对于 ref 引用元素横轴坐标
elH, // ref 引用元素高度
elW, // ref 引用元素宽度
});
}
};
// 监听 mousemove 事件
on(document, 'mousemove', moveHandler);
// 移除监听
return () => {
off(document, 'mousemove', moveHandler);
};
}, [ref]);
return state;
};
useEvent(name[, handler[, target[, options]]])
使用说明:监听某事件,触发事件回调,卸载时自动移除监听(目标/事件第三参数可配置-如捕获or冒泡阶段)
// interface isListenerType1 {
// addEventListener(name: string, handler: (event?: any) => void, ...args: any[]);
// removeEventListener(name: string, handler: (event?: any) => void, ...args: any[]);
// }
// UseEventTarget = isListenerType1 | isListenerType2
const useEvent = <T extends UseEventTarget>(
name: Parameters<AddEventListener<T>>[0], // 监听事件名称
handler?: null | undefined | Parameters<AddEventListener<T>>[1], // 事件handler
target: null | T | Window = defaultTarget, // 监听目标对象
options?: UseEventOptions<T> // 其余参数设置
) => {
useEffect(() => {
if (!handler) {
return;
}
if (!target) {
return;
}
if (isListenerType1(target)) {
on(target, name, handler, options);
} else if (isListenerType2(target)) {
target.on(name, handler, options);
}
return () => {
if (isListenerType1(target)) {
off(target, name, handler, options);
} else if (isListenerType2(target)) {
target.off(name, handler, options);
}
};
}, [name, handler, target, JSON.stringify(options)]);
};
useKey(key, handler, options, deps)
使用说明:监听键盘(默认为keydown)事件,触发事件回调,卸载时自动移除监听;本质上是封装使用了 useEvent
export type KeyPredicate = (event: KeyboardEvent) => boolean;
export type KeyFilter = null | undefined | string | ((event: KeyboardEvent) => boolean);
export type Handler = (event: KeyboardEvent) => void;
export interface UseKeyOptions<T extends UseEventTarget> {
event?: 'keydown' | 'keypress' | 'keyup';
target?: T | null;
options?: UseEventOptions<T>;
}
const createKeyPredicate = (keyFilter: KeyFilter): KeyPredicate =>
typeof keyFilter === 'function'
? keyFilter
: typeof keyFilter === 'string'
? (event: KeyboardEvent) => event.key === keyFilter
: keyFilter
? () => true
: () => false;
const useKey = <T extends UseEventTarget>(
key: KeyFilter, // key 可以是 string,也可以是函数
fn: Handler = noop, // 监听 key 按键事件 handler
opts: UseKeyOptions<T> = {}, // 自定义配置按键事件类型,监听目标元素,其余设置
deps: DependencyList = [key]
) => {
const { event = 'keydown', target, options } = opts;
const useMemoHandler = useMemo(() => {
const predicate: KeyPredicate = createKeyPredicate(key);
const handler: Handler = (handlerEvent) => {
if (predicate(handlerEvent)) {
return fn(handlerEvent);
}
};
return handler;
}, deps);
useEvent(event, useMemoHandler, target, options);
};
State
createGlobalState([initialState])
使用说明:利用闭包思想,创建了一个共享的 store;createGlobalState([initialState])
返回一个数组 – [state, stateSetter]
,stateSetter 的调用由共享 store 控制
export function createGlobalState<S>(initialState?: S) {
const store: {
state: S;
setState: (state: IHookStateSetAction<S>) => void;
setters: any[];
} = {
state: initialState instanceof Function ? initialState() : initialState,
setState(nextState: IHookStateSetAction<S>) {
// resolveHookState 的作用是处理 nextState 是函数类型和普通类型的情况
store.state = resolveHookState(nextState, store.state);
store.setters.forEach((setter) => setter(store.state));
},
setters: [],
};
return () => {
const [globalState, stateSetter] = useState<S | undefined>(store.state);
// store.setState 更新操作遍历调用了 stateSetter,因此会触发所有关联的 hook 更新
// store.setters 数组会移除'上一次绑定'调用过的 stateSetter
useEffectOnce(() => () => {
store.setters = store.setters.filter((setter) => setter !== stateSetter);
});
// 浏览器端于使用 useLayoutEffect,其余情况为 useEffect
useIsomorphicLayoutEffect(() => {
// stateSetter 不在 store.setters 数组则添加
if (!store.setters.includes(stateSetter)) {
store.setters.push(stateSetter);
}
});
return [globalState, store.setState];
};
}
createMemo(fn)
使用说明:接收一个函数,返回一个 memo 过的函数(函数调用结果会缓存效果)
const createMemo =
<T extends (...args: any) => any>(fn: T) =>
(...args: Parameters<T>) =>
useMemo<ReturnType<T>>(() => fn(...args), args);
例子,斐波那契数列
import {createMemo} from 'react-use';
const fibonacci = n => {
if (n === 0) return 0;
if (n === 1) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
};
const useMemoFibonacci = createMemo(fibonacci);
const Demo = () => {
const result = useMemoFibonacci(10);
return (
<div>
fib(10) = {result}
</div>
);
};
useRafState(initialState)
const useRafState = <S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>] => {
const frame = useRef(0);
const [state, setState] = useState(initialState);
// 相当于对 useState 进行了二次封装,保持每60帧 setState 更新
const setRafState = useCallback((value: S | ((prevState: S) => S)) => {
cancelAnimationFrame(frame.current);
frame.current = requestAnimationFrame(() => {
setState(value);
});
}, []);
useUnmount(() => {
cancelAnimationFrame(frame.current);
});
return [state, setRafState];
};
useFirstState()
使用说明:利用 ref 不变性,记录 hook 是否为首次挂载
export function useFirstMountState(): boolean {
const isFirst = useRef(true);
if (isFirst.current) {
isFirst.current = false;
return true;
}
return isFirst.current;
}
Side Effects
useAsyncFn(fnReturnPromise[, deps[, initialState]])
使用说明:传入一个返回 Promise 的异步函数,返回 [函数调用状态,useCallback包装待调用函数]
export default function useAsyncFn<T extends FunctionReturningPromise>(
fn: T,
deps: DependencyList = [],
initialState: StateFromFunctionReturningPromise<T> = { loading: false }
): AsyncFnReturn<T> {
const lastCallId = useRef(0);
const isMounted = useMountedState();
const [state, set] = useState<StateFromFunctionReturningPromise<T>>(initialState);
const callback = useCallback((...args: Parameters<T>): ReturnType<T> => {
const callId = ++lastCallId.current;
if (!state.loading) {
set((prevState) => ({ ...prevState, loading: true }));
}
return fn(...args).then(
(value) => {
isMounted() && callId === lastCallId.current && set({ value, loading: false });
return value;
},
(error) => {
isMounted() && callId === lastCallId.current && set({ error, loading: false });
return error;
}
) as ReturnType<T>;
}, deps);
return [state, callback as unknown as T];
}
useAsync(fnReturnPromise[, deps])
使用说明:传入一个返回 Promise 的异步函数,异步函数在 useEffect 执行,返回函数调用的状态
export default function useAsync<T extends FunctionReturningPromise>(
fn: T,
deps: DependencyList = []
) {
const [state, callback] = useAsyncFn(fn, deps, {
loading: true,
});
useEffect(() => {
callback();
}, [callback]);
return state;
}
useAsyncRetry(fnReturnPromise[, deps])
使用说明:useAsyncRetry 这里很巧妙的给 useAync 增加了一个 attempt 依赖,暴露的 retry 方法只需要更新 attempt,即可再次触发 useAsync 的调用(即异步函数的调用)
const useAsyncRetry = <T>(fn: () => Promise<T>, deps: DependencyList = []) => {
const [attempt, setAttempt] = useState<number>(0);
// !!!注意依赖数组添加了 attempt
const state = useAsync(fn, [...deps, attempt]);
const stateLoading = state.loading;
const retry = useCallback(() => {
if (stateLoading) {
if (process.env.NODE_ENV === 'development') {
console.log(
'You are calling useAsyncRetry hook retry() method while loading in progress, this is a no-op.'
);
}
return;
}
// retry 调用会更新 attempt
setAttempt((currentAttempt) => currentAttempt + 1);
}, [...deps, stateLoading]);
return { ...state, retry };
};
useFavicon(href)
使用说明:这个很简单,即通过 useEffect 副作用调用更新网页 icon
const useFavicon = (href: string) => {
useEffect(() => {
const link: HTMLLinkElement =
document.querySelector("link[rel*='icon']") || document.createElement('link');
link.type = 'image/x-icon';
link.rel = 'shortcut icon';
link.href = href;
document.getElementsByTagName('head')[0].appendChild(link);
}, [href]);
};
UI
useClickAway(ref, handler[, events])
使用说明:传入指定元素,和事件监听(默认监听 mousedown/touchstart
事件),点击指定元素以外的区域,触发监听回调
const defaultEvents = ['mousedown', 'touchstart'];
const useClickAway = <E extends Event = Event>(
ref: RefObject<HTMLElement | null>,
onClickAway: (event: E) => void,
events: string[] = defaultEvents
) => {
const savedCallback = useRef(onClickAway);
useEffect(() => {
savedCallback.current = onClickAway;
}, [onClickAway]);
useEffect(() => {
const handler = (event) => {
const { current: el } = ref;
el && !el.contains(event.target) && savedCallback.current(event);
};
for (const eventName of events) {
on(document, eventName, handler);
}
return () => {
for (const eventName of events) {
off(document, eventName, handler);
}
};
}, [events, ref]);
};
useFullscreen(ref, enabled [,options])
使用说明:传入需要全屏的元素,是否全屏设置,返回全屏的状态(true/false)
// 元素的 fullscreen 使用到了 screenfull 第三方库
const useFullscreen = (
ref: RefObject<Element>,
enabled: boolean,
options: FullScreenOptions = {}
): boolean => {
const { video, onClose = noop } = options;
const [isFullscreen, setIsFullscreen] = useState(enabled);
useIsomorphicLayoutEffect(() => {
if (!enabled) {
return;
}
if (!ref.current) {
return;
}
const onWebkitEndFullscreen = () => {
if (video?.current) {
off(video.current, 'webkitendfullscreen', onWebkitEndFullscreen);
}
onClose();
};
const onChange = () => {
if (screenfull.isEnabled) {
const isScreenfullFullscreen = screenfull.isFullscreen;
setIsFullscreen(isScreenfullFullscreen);
if (!isScreenfullFullscreen) {
onClose();
}
}
};
if (screenfull.isEnabled) {
// 支持元素全屏
try {
screenfull.request(ref.current);
setIsFullscreen(true);
} catch (error) {
onClose(error);
setIsFullscreen(false);
}
screenfull.on('change', onChange);
} else if (video && video.current && video.current.webkitEnterFullscreen) {
// 不支持元素全屏,且 options 传递了 video 元素
video.current.webkitEnterFullscreen();
on(video.current, 'webkitendfullscreen', onWebkitEndFullscreen);
setIsFullscreen(true);
} else {
onClose();
setIsFullscreen(false);
}
return () => {
// 卸载时退出全屏
setIsFullscreen(false);
if (screenfull.isEnabled) {
try {
screenfull.off('change', onChange);
screenfull.exit();
} catch {}
} else if (video && video.current && video.current.webkitExitFullscreen) {
off(video.current, 'webkitendfullscreen', onWebkitEndFullscreen);
video.current.webkitExitFullscreen();
}
};
}, [enabled, video, ref]);
return isFullscreen;
};
Lifecycle
useMountedState()
使用说明:用来判断 hook 当前是否属于挂载状态
export default function useMountedState(): () => boolean {
const mountedRef = useRef<boolean>(false);
const get = useCallback(() => mountedRef.current, []);
// 只在挂载时执行一次
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);
return get;
}
useEffectOnce(effect)
使用说明:如下注释
const useEffectOnce = (effect: EffectCallback) => {
// useEffect 第二个参数为空数组,因此 effect 副作用只会调用一次
useEffect(effect, []);
};
useUpdateEffect(effect, deps)
使用说明:传入副作用、依赖,副作用只在二次挂载更新时执行
const useUpdateEffect: typeof useEffect = (effect, deps) => {
const isFirstMount = useFirstMountState();
useEffect(() => {
if (!isFirstMount) {
return effect();
}
}, deps);
};
useUnmount(fn)
使用说明:传入的 fn 只在 hook 卸载时执行一次
const useUnmount = (fn: () => any): void => {
const fnRef = useRef(fn);
// update the ref each render
// so if it change the newest callback will be invoked
fnRef.current = fn;
// !!!关键点在这一步, 相当于 useEffect 销毁时的一次调用
// useEffect(() => {
// return () => fnRef.current()
// }, [])
useEffectOnce(() => () => fnRef.current());
};
useLifecycles(mount[, unmount])
使用说明:传入两个函数,前者在挂载时执行,后者在卸载时执行
const useLifecycles = (mount, unmount?) => {
useEffect(() => {
if (mount) {
mount();
}
return () => {
if (unmount) {
unmount();
}
};
}, []);
};
- useLogger
const useLogger = (componentName: string, ...rest) => {
useEffectOnce(() => {
console.log(`${componentName} mounted`, ...rest);
return () => console.log(`${componentName} unmounted`);
});
useUpdateEffect(() => {
console.log(`${componentName} updated`, ...rest);
});
};
Animation
- useUpdate()
使用说明:这个 hook 非常有意思,hook 调用返回一个函数,该函数执行则触发组件 re-render
const updateReducer = (num: number): number => (num + 1) % 1_000_000;
export default function useUpdate(): () => void {
// NOTE: 利用了 useReducer => [state, dispatch] 特性, state 的更新隐藏在 dipatch 函数不向外暴露
const [, update] = useReducer(updateReducer, 0);
return update;
}
useInterval(callback, delay)
const useInterval = (callback: Function, delay?: number | null) => {
const savedCallback = useRef<Function>(() => {});
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
if (delay !== null) {
const interval = setInterval(() => savedCallback.current(), delay || 0);
return () => clearInterval(interval);
}
return undefined;
}, [delay]);
};
- useTimeout(ms = 0)
使用说明:useTimeout
的源码很简单,内部调用的其实是 useUpdate
和 useTimeoutFn
这两个 hook;场景可用于设定 ms 时间后 re-render 组件
// useTimeout 返回值是一个包含三个函数的数组,[isReady, cancel, reset]
// isReady 函数调用会有3种返回值
// - true: timeout执行了 false: timeout 未执行 null: timeout 被取消了
export type UseTimeoutReturn = [() => boolean | null, () => void, () => void];
export default function useTimeout(ms: number = 0): UseTimeoutReturn {
// 该方法调用会触发组件 re-render
const update = useUpdate();
// !实际上是调用 useTimeoutFn
return useTimeoutFn(update, ms);
}
- useRaf(ms = 1e12, delay = 0)
使用说明:在 ms 时间内强制组件在每个 requestAnimationFrame
上重新渲染,返回经过的时间百分比。
const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect;
const useRaf = (ms: number = 1e12, delay: number = 0): number => {
const [elapsed, set] = useState<number>(0);
useIsomorphicLayoutEffect(() => {
let raf;
let timerStop;
let start;
const onFrame = () => {
const time = Math.min(1, (Date.now() - start) / ms);
set(time);
loop();
};
const loop = () => {
raf = requestAnimationFrame(onFrame);
};
const onStart = () => {
timerStop = setTimeout(() => {
cancelAnimationFrame(raf);
set(1);
}, ms);
start = Date.now();
loop();
};
const timerDelay = setTimeout(onStart, delay);
return () => {
clearTimeout(timerStop);
clearTimeout(timerDelay);
cancelAnimationFrame(raf);
};
}, [ms, delay]);
return elapsed;
};
总结
通过对 react-use 的源码的分析,我深刻感受到了 react hooks 对 react 原有开发模式(ClassComponent/HOC…)的颠覆以及其魅力,hook 的逻辑抽象组合复用能力杠杠的,且开发范式清晰明了,例如 react-use 很多 hooks 就是由一些简单的 hook 组合加成而来的(useUpdateEffect,useTimeout,useUpdate…),基础的 hook 遵循单一责任原则,可互相灵活配合成更强大的 hook;另外,阅读 react-use 源码也对 react hook 的执行机制更加了解、深刻。