关于前端:大家都能看得懂的源码-ahooks-是怎么处理-DOM-的

36次阅读

共计 3418 个字符,预计需要花费 9 分钟才能阅读完成。

本文是深入浅出 ahooks 源码系列文章的第十三篇,该系列已整顿成文档 - 地址。感觉还不错,给个 star 反对一下哈,Thanks。

本篇文章探讨一下 ahooks 对 DOM 类 Hooks 应用标准,以及源码中是如何去做解决的。

DOM 类 Hooks 应用标准

这一章节,大部分参考官网文档的 DOM 类 Hooks 应用标准。

第一点,ahooks 大部分 DOM 类 Hooks 都会接管 target 参数,示意要解决的元素。

target 反对三种类型 React.MutableRefObject(通过 useRef 保留的 DOM)、HTMLElement、() => 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 元素,这一点次要兼容以上第一点的入参标准。

  • 如果是函数,则取执行完后的后果。
  • 如果领有 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。留神这里调用的时候, 没传第二个参数,也就是每次都会执行
  • hasInitRef 判断是否曾经初始化。lastElementRef 记录的是最初一次 target 元素的列表。lastDepsRef 记录的是最初一次的依赖。unLoadRef 是执行完 effect 函数(对应的就是 useEffect 中的 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;
};

思考与总结

一个优良的工具库应该有本人的一套输入输出标准,一来可能反对更多的场景,二来能够更好的在外部进行封装解决,三来使用者可能更加疾速相熟和应用相应的性能,能做到触类旁通。

本文已收录到集体博客中,欢送关注~

正文完
 0