关于react.js:解读-ahooks-源码系列-开篇如何获取和监听-DOM-元素

6次阅读

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

前言

因为在工作中自定义 Hook 场景写的较多,当实现某个通用场景性能时,可能没想过有已实现好的 Hook 封装或者压根没想去从 Hooks 库外面找,然而社区好的实现应用起来是能够进步开发效率和缩小 bug 率的。

公司我的项目中有依赖库 ahooks,但我用的次数不多,于是有了想具体理解 ahooks 的打算,更次要是为了更加纯熟抽离与实现一些场景 Hook,学习如何更好的自定义 Hook,便有开始浏览 ahooks 源码的打算了。

学习 ahooks 源码的益处

在我看来,学习 ahooks 常见 Hooks 封装有以下益处:

  • 相熟如何依据需要去提炼相应的 Hooks,将通用逻辑进行封装
  • 解说源码实现思路,提炼外围实现,通过学习源码学习自定义 Hooks 最佳实际
  • 深刻学习特定的场景 Hooks,我的项目开发中一点通,应用时更得心应手

对于源码系列

本系列文章基于 ahooks 版本 v3.7.4,后续会相继输入 ahooks 源码解读的系列文章。

依照 ahooks 官网的分类,我目前先从 DOM 篇开始看起,DOM 篇包含的 Hooks 如下:

  • useEventListener:优雅的应用 addEventListener。
  • useClickAway:监听指标元素外的点击事件。
  • useDocumentVisibility:优雅的应用 addEventListener。
  • useDrop & useDrag:解决元素拖拽的 Hook。
  • useEventTarget:常见表单控件 (通过 e.target.value 获取表单值) 的 onChange 跟 value 逻辑封装,反对自定义值转换和重置性能。
  • useExternal:动静注入 JS 或 CSS 资源,useExternal 能够保障资源全局惟一。
  • useTitle:用于设置页面题目。
  • useFavicon:设置页面的 favicon。
  • useFullscreen:治理 DOM 全屏的 Hook。
  • useHover:监听 DOM 元素是否有鼠标悬停。
  • useMutationObserver:一个监听指定的 DOM 树发生变化的 Hook。
  • useInViewport:察看元素是否在可见区域,以及元素可见比例。
  • useKeyPress:监听键盘按键,反对组合键,反对按键别名。
  • useLongPress:监听指标元素的长按事件。
  • useMouse:监听鼠标地位。
  • useResponsive:获取响应式信息。
  • useScroll:监听元素的滚动地位。
  • useSize:监听 DOM 节点尺寸变动的 Hook。
  • useFocusWithin:监听以后焦点是否在某个区域之内,同 css 属性: focus-within。

因为内容较多,DOM 篇会分成几篇文章输入,这样每篇读起来既不太耗时也能疾速过一遍。文章会在解读源码的根底上,也会把波及到的 JS 基础知识拎进去,在学源码的过程也能查漏补缺根底。

回到本文正题,在看 DOM 篇分类下的 Hooks 时,我发现 getTargetElement 办法和 useEffectWithTarget 外部 Hook 应用较多,所以在讲源码之前先来理解这两个 Hook。

如何获取 DOM 元素

三种类型的 target

在 DOM 类 Hooks 应用标准中提到:

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

target 反对三种类型 React.MutableRefObjectHTMLElement() => HTMLElement

  1. React.MutableRefObject
export default () => {const ref = useRef(null)
  const isHovering = useHover(ref)
  return <div ref={ref}>{isHovering ? 'hover' : 'leaveHover'}</div>
}
  1. HTMLElement
export default () => {const isHovering = useHover(document.getElementById('test'))
  return <div id="test">{isHovering ? 'hover' : 'leaveHover'}</div>
}
  1. 反对 () => HTMLElement,个别实用在 SSR 场景
export default () => {const isHovering = useHover(() => document.getElementById('test'))
  return <div id="test">{isHovering ? 'hover' : 'leaveHover'}</div>
}

getTargetElement

为了兼容以上三种类型入参,ahooks 封装了 getTargetElement – 获取指标 DOM 元素 办法。咱们来看看代码做了什么:

  1. 判断是否为浏览器环境,不是则返回 undefined
  2. 判断指标元素是否为空,为空则返回函数参数指定的默认元素
  3. 外围:

    • 如果是函数,则返回函数执行后的后果
    • 如果有 current 属性,则返回 .current 属性的值,兼容 React.MutableRefObject 类型
    • 以上都不是,则代表一般 DOM 元素,间接返回
export function getTargetElement<T extends TargetType>(target: BasicTarget<T>, defaultElement?: T) {
  // 判断是否为浏览器环境
  if (!isBrowser) {return undefined;}

  // 指标元素为空则返回函数参数指定的默认元素
  if (!target) {return defaultElement;}

  let targetElement: TargetValue<T>;

  // 反对函数执行返回
  if (isFunction(target)) {targetElement = target();
  } else if ('current' in target) {
    // 兼容 React.MutableRefObject 类型,返回 .current 属性的值
    targetElement = target.current;
  } else {
    // 一般 DOM 元素
    targetElement = target;
  }

  return targetElement;
}

对应的 TS 类型:

type TargetValue<T> = T | undefined | null

type TargetType = HTMLElement | Element | Window | Document

export type BasicTarget<T extends TargetType = Element> =
  | (() => TargetValue<T>)
  | TargetValue<T>
  | MutableRefObject<TargetValue<T>>

监听 DOM 元素

target 反对动态变化

ahooks 的 DOM 类 Hooks 应用标准第二条点指出:

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>
    </>
  )
}

useEffectWithTarget

为了满足上述条件,ahooks 外部则封装 useEffectWithTargetpackages/hooks/src/utils/useEffectWithTarget.ts),来看这个文件的代码:

import {useEffect} from 'react'
import createEffectWithTarget from './createEffectWithTarget'

const useEffectWithTarget = createEffectWithTarget(useEffect)

export default useEffectWithTarget

看到它理论用了 createEffectWithTarget 办法,传入的参数是 useEffectpackages/hooks/src/utils/createEffectWithTarget.ts

  • createEffectWithTarget 承受参数 useEffect 或 useLayoutEffect,返回 useEffectWithTarget 函数
  • useEffectWithTarget 函数接管三个参数:前两个参数是 effect 和 deps(与 useEffect 参数统一),第三个参数则兼容了 DOM 元素的三种类型,可传 一般 DOM/ref 类型 / 函数类型

useEffectWithTarget 实现思路:

  1. 应用 useEffect/useLayoutEffect 监听,外部不传第二个参数依赖项,每次更新都会执行该副作用函数
  2. 通过 hasInitRef 判断是否是第一次执行,是则初始化:记录最初一次指标元素列表和依赖项,执行 effect 函数
  3. 因为该 useEffectType 函数体每次更新都会执行,所以每次都拿到最新的 targets 和 deps,所以后续执行可与第 2 点记录的最初一次的 ref 值进行比对
  4. 非首次执行:则判断元素列表长度或指标元素或者依赖发生变化,变动了则执行更新流程:执行上一次返回的卸载函数,更新最新值,从新执行 effect
  5. 组件卸载:执行 unLoadRef.current?.() 卸载函数,重置 hasInitRef
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>()

    // useEffectType:代表 useEffect 或 useLayoutEffect,每次更新都会执行该函数
    useEffectType(() => {const targets = Array.isArray(target) ? target : [target]
      const els = targets.map((item) => getTargetElement(item)) // 获取 DOM 元素列表

      // 首次执行:初始化
      if (!hasInitRef.current) {
        hasInitRef.current = true
        lastElementRef.current = els // 最初一次执行的相应的 target 元素
        lastDepsRef.current = deps // 最初一次执行的相应的依赖

        unLoadRef.current = effect() // 执行内部传入的 effect 函数,返回卸载函数
        return
      }

      // 非首次执行:判断元素列表长度或指标元素或者依赖发生变化
      if (
        els.length !== lastElementRef.current.length ||
        !depsAreSame(els, lastElementRef.current) ||
        !depsAreSame(deps, lastDepsRef.current)
      ) {
        // 依赖产生变更了,相当于走 useEffect 更新流程
        unLoadRef.current?.()
        lastElementRef.current = els
        lastDepsRef.current = deps
        unLoadRef.current = effect() // 再次执行 effect,赋值卸载函数给 unLoadRef}
    }) // 没有传第二个参数,则每次都会执行

    // 卸载操作 Hook
    useUnmount(() => {unLoadRef.current?.() // 执行卸载操作
      // for react-refresh
      hasInitRef.current = false
    })
  }

  return useEffectWithTarget
}

depsAreSame 实现:

import type {DependencyList} from 'react'

export default function depsAreSame(
  oldDeps: DependencyList,
  deps: DependencyList,
): boolean {if (oldDeps === deps) return true // 浅比拟
  for (let i = 0; i < oldDeps.length; i++) {if (!Object.is(oldDeps[i], deps[i])) return false
  }
  return true
}

这样应用起来跟 useEffect 的区别就是有第三个参数——监听的 DOM 元素

参考文章

  • ahooks 是怎么解决 DOM 的?
正文完
 0