likes
comments
collection
share

如何基于 vue3.x 编写自己的 hook

作者站长头像
站长
· 阅读数 11

什么是 hooks

函数式编程在前端开发中越来越流行,尤其是在现代前端框架 Vue3.xReact 16+ 中。它的优点包括代码可读性、可维护性、可测试性和复用性。

学习如何利用框架提供的钩子(hooks)编写自定义钩子函数是非常重要的技能之一。通过编写自定义钩子函数,我们可以满足特定需求,使我们的代码更加灵活和可扩展。

掌握函数式编程和钩子的使用,能够提高我们的开发效率,同时提供更好的用户体验和代码质量。在使用 Vue3.xReact 16+ 等现代前端框架时,函数式编程和自定义钩子函数将成为我们必备的技能。

优秀的 vue3.x hooks 库

两个出色的开源库,它们几乎可以满足各种场景的需求。然而,学习如何独立编写代码也是非常重要的。通过亲自动手编写代码,我们能够深入理解底层原理,培养自己的解决问题的能力。

起步

假设你已经了解并且掌握 vue3.x ts,所以不做过多的赘述。废话不多说,直接上手~~~

useElementBounding 方法

这里以该方法作为例子讲解。

功能分析

根据方法名称,我们可以直观地了解到这个方法的作用是用于动态获取元素的坐标和尺寸信息。

该方法提供了一种便捷的方式来获取元素的位置和大小。通过调用这个方法,我们可以动态地获取元素的坐标、宽度、高度等信息,以便在开发过程中进行相应的操作和布局调整。

参数分析

  1. 待监听的 dom
  2. 配置监听条件 options

开发

配置项类型

interface UseElementBoundingOptions {
  /**
   *
   * When the component is mounted, initialize all values to 0
   *
   * @default true
   */
  reset?: boolean
  /**
   *
   * windowResize
   *
   * @default true
   */
  windowResize?: boolean
  /**
   *
   * windowScroll
   *
   * @default true
   */
  windowScroll?: boolean
  /**
   *
   * immediate
   *
   * @default true
   */
  immediate?: boolean
}

interface UseElementBoundingReturnType {
  width: Ref<number>
  height: Ref<number>
  top: Ref<number>
  left: Ref<number>
  bottom: Ref<number>
  right: Ref<number>
}

辅助函数

type TargetValue<T> = T | undefined | null

type TargetType = HTMLElement | Element | Window | Document | ComponentPublicInstance

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

function getTargetElement<T extends TargetType>(target: BasicTarget<T>, defaultElement?: T) {
  if (!isBrowser) {
    return undefined
  }

  if (!target) {
    return defaultElement
  }

  let targetElement: TargetValue<T>
  
  if (typeof target === 'function') {
    targetElement = target()
  } else if (isRef(target)) {
    targetElement = (target.value as ComponentPublicInstance)?.$el ?? target.value
  } else {
    targetElement = target
  }
  return targetElement
}

正式开发

export default function useElementBounding(
  target: BasicTarget,
  options?: UseElementBoundingOptions,
): UseElementBoundingReturnType {
  const { reset = true, windowResize = true, windowScroll = true, immediate = true } = options ?? {}
  const size = reactive({
    width: 0,
    height: 0,
    top: 0,
    left: 0,
    bottom: 0,
    right: 0,
  })
    
  // 定义更新函数
  const update = () => {
    // 获取 dom
    const targetDom = getTargetElement(target)

    if (!targetDom) {
      // 组件安装后,将所有值初始化为 0
      if (reset) {
        Object.keys(size).forEach(key => {
          if (keyisUseElementBoundingReturnTypeKey(key))
            size[key] = 0
        })
      }

      return
    }

    if (targetDom) {
      // 使用 getBoundingClientRect 方法,获取元素信息
      const { width, height, top, left, bottom, right } = targetDom.getBoundingClientRect()

      size.width = width
      size.height = height
      size.top = top
      size.left = left
      size.bottom = bottom
      size.right = right
    }
  }

  // 窗口尺寸发生更改时触发更新
  if (windowResize) {
    useEventListener('resize', update, {
      // 事件监听器不会调用 preventDefault() 方法来阻止默认行为。这样可以提高滚动的性能,并且减少滚动操作的延迟。
      passive: true,
    })
  }

  // 窗口滚动时触发更新
  if (windowScroll) {
    useEventListener('scroll', update, {
      capture: true,
      passive: true,
    })
  }

  // 元素尺寸更改时触发更新
  useResizeObserver(target, update)
  // 代理对象发生更改时触发更新
  watch(() => getTargetElement(target), update)

  onMounted(() => {
    immediate && update()
  })

  return {
    ...toRefs(size),
  }
}

完整代码

import { onMounted, reactive, toRefs, Ref, watch } from 'vue'
import useResizeObserver from '../useResizeObserver'
import useEventListener from '../useEventListener'

import { BasicTarget, getTargetElement } from '../utils/domTarget'

export interface UseElementBoundingOptions {
  /**
   *
   * When the component is mounted, initialize all values to 0
   *
   * @default true
   */
  reset?: boolean
  /**
   *
   * windowResize
   *
   * @default true
   */
  windowResize?: boolean
  /**
   *
   * windowScroll
   *
   * @default true
   */
  windowScroll?: boolean
  /**
   *
   * immediate
   *
   * @default true
   */
  immediate?: boolean
}

function keyisUseElementBoundingReturnTypeKey(key: string): key is keyof UseElementBoundingReturnType {
  return ['width', 'height', 'top', 'left', 'bottom', 'right'].includes(key)
}

export interface UseElementBoundingReturnType {
  width: Ref<number>
  height: Ref<number>
  top: Ref<number>
  left: Ref<number>
  bottom: Ref<number>
  right: Ref<number>
}

export default function useElementBounding(
  target: BasicTarget,
  options?: UseElementBoundingOptions,
): UseElementBoundingReturnType {
  const { reset = true, windowResize = true, windowScroll = true, immediate = true } = options ?? {}
  const size = reactive({
    width: 0,
    height: 0,
    top: 0,
    left: 0,
    bottom: 0,
    right: 0,
  })
    
  // 定义更新函数
  const update = () => {
    // 获取 dom
    const targetDom = getTargetElement(target)

    if (!targetDom) {
      // 组件安装后,将所有值初始化为 0
      if (reset) {
        Object.keys(size).forEach(key => {
          if (keyisUseElementBoundingReturnTypeKey(key))
            size[key] = 0
        })
      }

      return
    }

    if (targetDom) {
      // 使用 getBoundingClientRect 方法,获取元素信息
      const { width, height, top, left, bottom, right } = targetDom.getBoundingClientRect()

      size.width = width
      size.height = height
      size.top = top
      size.left = left
      size.bottom = bottom
      size.right = right
    }
  }

  // 窗口尺寸发生更改时触发更新
  if (windowResize) {
    useEventListener('resize', update, {
      // 事件监听器不会调用 preventDefault() 方法来阻止默认行为。这样可以提高滚动的性能,并且减少滚动操作的延迟。
      passive: true,
    })
  }

  // 窗口滚动时触发更新
  if (windowScroll) {
    useEventListener('scroll', update, {
      capture: true,
      passive: true,
    })
  }

  // 元素尺寸更改时触发更新
  useResizeObserver(target, update)
  // 代理对象发生更改时触发更新
  watch(() => getTargetElement(target), update)

  onMounted(() => {
    immediate && update()
  })

  return {
    ...toRefs(size),
  }
}

啰嗦两句

其实代码量并不多,核心思想就是利用 vue3.x 的响应式方法进行封装。该代码 源码地址

卖个瓜

vue-hooks-plus 也是小编参与贡献与维护的的一个开源 vue3 hooks 库,可以来看看,没事还可以点个赞~~~谢谢~~~