ahooks 中 DOM 类 Hooks 的规范与源码解析
本文深入解析 ahooks 中 DOM 类 Hooks 的使用规范,重点探讨 target 参数的三种类型及动态变化机制。通过分析 getTargetElement 与 createEffectWithTarget 源码,揭示其如何兼容 React.MutableRefObject、HTMLElement 及函数返回值,并实现依赖监听与生命周期管理,帮助开发者理解自定义 Hooks 的封装逻辑。
前言
本文旨在达成以下目标:
- 深入理解 React Hooks 的核心机制。
- 掌握自定义 Hooks 的抽象方法,构建个人 React Hooks 工具库。
- 养成阅读优秀源码的习惯,工具库源码是学习的优质素材。
本文将探讨 ahooks 中 DOM 类 Hooks 的使用规范,并解析其源码实现逻辑。
DOM 类 Hooks 使用规范
本节内容主要参考 ahooks 官方文档中关于 DOM 类 Hooks 的规范说明。
首先,ahooks 中大多数 DOM 类 Hooks 均接收 target 参数,用于指定需要处理的 DOM 元素。
target 参数支持三种类型:React.MutableRefObject(通过 useRef 创建的引用对象)、HTMLElement(原生 DOM 节点)以及 () => HTMLElement(通常用于服务端渲染 SSR 场景的函数)。
其次,DOM 类 Hooks 的 target 支持动态变化。
具体示例如下:
export default () => {
const [boolean, { toggle }] = useBoolean();
const ref = useRef(null);
const ref2 = useRef(null);
const isHovering = useHover(boolean ? ref : ref2);
return (
<>
<div ref={ref}>{isHovering ? 'hover' : 'leaveHover'}</div>
<div ref={ref2}>{isHovering ? 'hover' : 'leaveHover'}</div>
</>
);
};
那么 ahooks 是如何处理上述两种情况的呢?
getTargetElement
该函数用于获取对应的 DOM 元素,主要目的是兼容上述 target 参数的入参规范。
- 如果 target 是函数,则执行该函数并获取结果。
- 如果 target 拥有 current 属性,则获取 current 属性的值,以此兼容
React.MutableRefObject类型。 - 最后一种情况是普通的 DOM 元素,直接返回即可。
export function getTargetElement<T extends TargetType>(target: BasicTarget<T>, defaultElement?: T) {
// 省略部分代码...
let targetElement: TargetValue<T>;
if (isFunction(target)) {
// 支持函数获取
targetElement = target();
// 假如 ref,则返回 current
} else if ('current' in target) {
targetElement = target.current;
// 支持 DOM
} else {
targetElement = target;
}
return targetElement;
}
useEffectWithTarget
该方法主要用于支持 target 的动态变化特性。
其中 packages/hooks/src/utils/useEffectWithTarget.ts 文件使用了 useEffect。
import { useEffect } from 'react';
import createEffectWithTarget from './createEffectWithTarget';
const useEffectWithTarget = createEffectWithTarget(useEffect);
export default useEffectWithTarget;
另外,packages/hooks/src/utils/useLayoutEffectWithTarget.ts 文件使用了 useLayoutEffect。
import { useLayoutEffect } from 'react';
import createEffectWithTarget from './createEffectWithTarget';
const useEffectWithTarget = createEffectWithTarget(useLayoutEffect);
export default useEffectWithTarget;两者均调用 createEffectWithTarget,仅传入的参数不同。
接下来重点分析 createEffectWithTarget 函数:
- createEffectWithTarget 返回的 useEffectWithTarget 函数接收三个参数,前两个与 useEffect 一致,第三个参数为 target。
- useEffectType 参数指定使用 useEffect 或 useLayoutEffect。注意此处调用时未传入第二个参数(依赖数组),这意味着每次渲染都会执行 effect。
- hasInitRef 用于判断是否已初始化;lastElementRef 记录最后一次 target 元素的列表;lastDepsRef 记录上一次的依赖数组;unLoadRef 存储 effect 函数的返回值,用于组件卸载时执行清理逻辑。
- 首次执行时,运行相应逻辑,并记录最后一次执行的 target 元素及依赖。
- 后续每次执行时,判断目标元素或依赖是否发生变化,若发生变化则执行对应的 effect 函数,并更新最后一次执行的依赖记录。
- 组件卸载时,执行 unLoadRef.current?.() 函数,并将 hasInitRef 重置为 false。
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>();
// useEffect 或者 useLayoutEffect
useEffectType(() => {
// 处理 DOM 目标元素
const targets = Array.isArray(target) ? target : [target];
const els = targets.map((item) => getTargetElement(item));
// init run
// 首次初始化的时候执行
if (!hasInitRef.current) {
hasInitRef.current = true;
lastElementRef.current = els;
lastDepsRef.current = deps;
// 执行回调中的 effect 函数
unLoadRef.current = effect();
return;
}
// 非首次执行的逻辑
if (
// 目标元素或者依赖发生变化
els.length !== lastElementRef.current.length ||
!depsAreSame(els, lastElementRef.current) ||
!depsAreSame(deps, lastDepsRef.current)
) {
// 执行上次返回的结果
unLoadRef.current?.();
// 更新
lastElementRef.current = els;
lastDepsRef.current = deps;
unLoadRef.current = effect();
}
});
useUnmount(() => {
// 卸载
unLoadRef.current?.();
// for react-refresh
hasInitRef.current = false;
});
};
return useEffectWithTarget;
};思考与总结
优秀的工具库应具备统一的输入输出规范,这不仅能支持更多场景,便于内部封装,也能让用户快速熟悉并使用功能,做到举一反三。
至此,关于 JavaScript 中 ahooks 处理 DOM 方法的介绍结束,更多相关内容请搜索本站历史文章或继续浏览后续文章,希望大家多多支持!
在 React 或 Vue 项目中集成 p5.js 的实例模式教程
2026-11-03 21:15:55
React 组件怎么连接 dva 状态?connect 高阶函数用法解析
2026-11-03 10:18:46
Fuse.js 模糊搜索实战:从基础配置到 React 实时联想
2026-10-31 10:29:51
详解JS如何解决大数据下滚动页面卡顿问题
2026-10-30 22:20:38
怎么解决 Next.js 开发中的水合不匹配(Hydration Mismatch)错误
2026-10-30 19:14:49







