这个系列是将 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 源码吧~ 今天进入 Dom 部分 hooks,Dom 部分共有 16 个 hook,封装了各种 Dom 相关的常用场景。
Dom
useEventTarget
“v-model yyds”
/// ...
function useEventTarget<T, U = T>(options?: Options<T, U>) {
const { initialValue, transformer } = options || {};
const [value, setValue] = useState(initialValue);
const reset = useCallback(() => setValue(initialValue), []);
const transformerRef = useRef(transformer);
transformerRef.current = transformer;
const onChange = useCallback((e: EventTarget<U>) => {
/// 默认情况下取值,如果是 checkbox 就需要自己写 transformer 了
const _value = e.target.value;
if (typeof transformerRef.current === 'function') {
return setValue(transformerRef.current(_value));
}
// no transformer => U and T should be the same
return setValue((_value as unknown) as T);
}, []);
return [
value,
{
onChange,
reset,
},
] as const;
}
export default useEventTarget;
复制代码
useEventListener
不用考虑销毁的事件监听
import { MutableRefObject } from 'react';
export type BasicTarget<T = HTMLElement> =
| (() => T | null)
| T
| null
| MutableRefObject<T | null | undefined>;
type TargetElement = HTMLElement | Element | Document | Window;
export function getTargetElement(
target?: BasicTarget<TargetElement>,
defaultElement?: TargetElement,
): TargetElement | undefined | null {
/// target不存在,取默认元素
if (!target) {
return defaultElement;
}
let targetElement: TargetElement | undefined | null;
/// 如果是方法就运行一下,如果有 current 属性值则取 current(针对Ref),否则直接去入参
if (typeof target === 'function') {
targetElement = target();
} else if ('current' in target) {
targetElement = target.current;
} else {
targetElement = target;
}
return targetElement;
}
复制代码
import { useEffect, useRef } from 'react';
import { BasicTarget, getTargetElement } from '../utils/dom';
/// ...
function useEventListener(eventName: string, handler: Function, options: Options = {}) {
const handlerRef = useRef<Function>();
handlerRef.current = handler;
/// 组件渲染之后绑定事件,销毁时清除绑定
useEffect(() => {
const targetElement = getTargetElement(options.target, window)!;
if (!targetElement.addEventListener) {
return;
}
const eventListener = (
event: Event,
): EventListenerOrEventListenerObject | AddEventListenerOptions => {
return handlerRef.current && handlerRef.current(event);
};
targetElement.addEventListener(eventName, eventListener, {
capture: options.capture,
once: options.once,
passive: options.passive,
});
return () => {
targetElement.removeEventListener(eventName, eventListener, {
capture: options.capture,
});
};
}, [eventName, options.target, options.capture, options.once, options.passive]);
}
export default useEventListener;
复制代码
useKeyPress
“处理键盘事件,我们是专业的”
专门处理键盘事件的 hook,能够很方便的处理组合键。
import { useEffect, useRef } from 'react';
import { BasicTarget, getTargetElement } from '../utils/dom';
/// ...
// 键盘事件 keyCode 别名
const aliasKeyCodeMap: any = {
esc: 27,
tab: 9,
enter: 13,
space: 32,
up: 38,
left: 37,
right: 39,
down: 40,
delete: [8, 46],
};
// 键盘事件 key 别名
const aliasKeyMap: any = {
esc: 'Escape',
tab: 'Tab',
enter: 'Enter',
space: ' ',
// IE11 uses key names without `Arrow` prefix for arrow keys.
up: ['Up', 'ArrowUp'],
left: ['Left', 'ArrowLeft'],
right: ['Right', 'ArrowRight'],
down: ['Down', 'ArrowDown'],
delete: ['Backspace', 'Delete'],
};
// 修饰键
const modifierKey: any = {
ctrl: (event: KeyboardEvent) => event.ctrlKey,
shift: (event: KeyboardEvent) => event.shiftKey,
alt: (event: KeyboardEvent) => event.altKey,
meta: (event: KeyboardEvent) => event.metaKey,
};
/// 不是返回空对象,是定义了一个空函数,返回的是 undefined
// 返回空对象
const noop = () => {};
/**
* 判断对象类型
* @param [obj: any] 参数对象
* @returns String
*/
function isType(obj: any) {
return Object.prototype.toString
.call(obj)
.replace(/^\[object (.+)\]$/, '$1')
.toLowerCase();
}
/**
* 判断按键是否激活
* @param [event: KeyboardEvent]键盘事件
* @param [keyFilter: any] 当前键
* @returns Boolean
*/
function genFilterKey(event: any, keyFilter: any) {
// 浏览器自动补全 input 的时候,会触发 keyDown、keyUp 事件,但此时 event.key 等为空
if (!event.key) {
return false;
}
const type = isType(keyFilter);
// 数字类型直接匹配事件的 keyCode
if (type === 'number') {
return event.keyCode === keyFilter;
}
// 字符串依次判断是否有组合键
const genArr = keyFilter.split('.');
let genLen = 0;
for (const key of genArr) {
// 组合键
const genModifier = modifierKey[key];
// key 别名
const aliasKey = aliasKeyMap[key];
// keyCode 别名
const aliasKeyCode = aliasKeyCodeMap[key];
/**
* 满足以上规则
* 1. 自定义组合键别名
* 2. 自定义 key 别名
* 3. 自定义 keyCode 别名
* 4. 匹配 key 或 keyCode
*/
if (
(genModifier && genModifier(event)) ||
(aliasKey && isType(aliasKey) === 'array'
? aliasKey.includes(event.key)
: aliasKey === event.key) ||
(aliasKeyCode && isType(aliasKeyCode) === 'array'
? aliasKeyCode.includes(event.keyCode)
: aliasKeyCode === event.keyCode) ||
event.key.toUpperCase() === key.toUpperCase()
) {
genLen++;
}
}
return genLen === genArr.length;
}
/**
* 键盘输入预处理方法
* @param [keyFilter: any] 当前键
* @returns () => Boolean
*/
function genKeyFormater(keyFilter: any): KeyPredicate {
const type = isType(keyFilter);
if (type === 'function') {
return keyFilter;
}
if (type === 'string' || type === 'number') {
return (event: KeyboardEvent) => genFilterKey(event, keyFilter);
}
/// 如果是数组,则只要有一个符合就可以触发
if (type === 'array') {
return (event: KeyboardEvent) => keyFilter.some((item: any) => genFilterKey(event, item));
}
return keyFilter ? () => true : () => false;
}
const defaultEvents: Array<keyEvent> = ['keydown'];
function useKeyPress(
keyFilter: KeyFilter,
eventHandler: EventHandler = noop,
option: EventOption = {},
) {
const { events = defaultEvents, target } = option;
const callbackRef = useRef(eventHandler);
callbackRef.current = eventHandler;
useEffect(() => {
const callbackHandler = (event) => {
/// 如果事件对象符合传入的组合键要求则触发回调
const genGuard: KeyPredicate = genKeyFormater(keyFilter);
if (genGuard(event)) {
return callbackRef.current(event);
}
};
const el = getTargetElement(target, window)!;
/// 默认绑定在 keyDown 事件上,可以自定义
for (const eventName of events) {
el.addEventListener(eventName, callbackHandler);
}
return () => {
for (const eventName of events) {
el.removeEventListener(eventName, callbackHandler);
}
};
}, [events, keyFilter, target]);
}
export default useKeyPress;
复制代码
useScroll
“处理滚动,我也是专业的”
import { useEffect, useState } from 'react';
import usePersistFn from '../usePersistFn';
import { BasicTarget, getTargetElement } from '../utils/dom';
/// ...
function useScroll(target?: Target, shouldUpdate: ScrollListenController = () => true): Position {
const [position, setPosition] = useState<Position>({
left: NaN,
top: NaN,
});
const shouldUpdatePersist = usePersistFn(shouldUpdate);
useEffect(() => {
const el = getTargetElement(target, document);
if (!el) return;
function updatePosition(currentTarget: Target): void {
let newPosition;
if (currentTarget === document) {
if (!document.scrollingElement) return;
newPosition = {
left: document.scrollingElement.scrollLeft,
top: document.scrollingElement.scrollTop,
};
} else {
newPosition = {
left: (currentTarget as HTMLElement).scrollLeft,
top: (currentTarget as HTMLElement).scrollTop,
};
}
if (shouldUpdatePersist(newPosition)) setPosition(newPosition);
}
updatePosition(el as Target);
function listener(event: Event): void {
if (!event.target) return;
updatePosition(event.target as Target);
}
el.addEventListener('scroll', listener);
return () => {
el.removeEventListener('scroll', listener);
};
}, [target, shouldUpdatePersist]);
return position;
}
export default useScroll;
复制代码
useSize
实时拿到目标的最新尺寸数据,基于 ResizeObserver api
import { useState, useLayoutEffect } from 'react';
import ResizeObserver from 'resize-observer-polyfill';
import { getTargetElement, BasicTarget } from '../utils/dom';
type Size = { width?: number; height?: number };
function useSize(target: BasicTarget): Size {
const [state, setState] = useState<Size>(() => {
const el = getTargetElement(target);
return {
width: ((el || {}) as HTMLElement).clientWidth,
height: ((el || {}) as HTMLElement).clientHeight,
};
});
/// 需要拿到 dom 数据,所以使用了 useLayoutEffect
useLayoutEffect(() => {
const el = getTargetElement(target);
if (!el) {
return () => {};
}
/// https://developer.mozilla.org/zh-CN/docs/Web/API/ResizeObserver
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
setState({
width: entry.target.clientWidth,
height: entry.target.clientHeight,
});
});
});
resizeObserver.observe(el as HTMLElement);
return () => {
resizeObserver.disconnect();
};
}, [target]);
return state;
}
export default useSize;
复制代码
useClickAway
modal 弹框经常会用到
import { useEffect, useRef } from 'react';
import { BasicTarget, getTargetElement } from '../utils/dom';
// 鼠标点击事件,click 不会监听右键
const defaultEvent = 'click';
type EventType = MouseEvent | TouchEvent;
export default function useClickAway(
onClickAway: (event: EventType) => void,
target: BasicTarget | BasicTarget[],
eventName: string = defaultEvent,
) {
const onClickAwayRef = useRef(onClickAway);
onClickAwayRef.current = onClickAway;
useEffect(() => {
const handler = (event: any) => {
const targets = Array.isArray(target) ? target : [target];
if (
targets.some((targetItem) => {
const targetElement = getTargetElement(targetItem) as HTMLElement;
/// https://developer.mozilla.org/zh-CN/docs/Web/API/Node/contains
/// Node.contains()返回的是一个布尔值,来表示传入的节点是否是 node 的后代节点或是 node 节点本身
return !targetElement || targetElement?.contains(event.target);
})
) {
return;
}
/// 一个目标都没有命中,则触发回调
onClickAwayRef.current(event);
};
document.addEventListener(eventName, handler);
return () => {
document.removeEventListener(eventName, handler);
};
}, [target, eventName]);
}
复制代码
参考资料
以上内容由于本人水平问题难免有误,欢迎大家进行讨论反馈。