ahooks 中 DOM 类 Hooks 的规范与源码解析

作者:晚风写码时间:2026-10-25 13:01:35浏览:519

本文深入解析 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 方法的介绍结束,更多相关内容请搜索本站历史文章或继续浏览后续文章,希望大家多多支持!

作者最新文章
相关文章
更多