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

什么是 hooks

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

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

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

优秀的 vue3.x hooks 库

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

起步

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

useElementBounding 方法

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

功能分析

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

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

参数分析

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

开发

配置项类型

ts 复制代码
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>
}

辅助函数

ts 复制代码
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
}

正式开发

ts 复制代码
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),
  }
}

完整代码

ts 复制代码
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 库,可以来看看,没事还可以点个赞~~~谢谢~~~

相关推荐
qq_5895681043 分钟前
Echarts+vue电商平台数据可视化——后台实现笔记
vue.js·信息可视化·echarts
5hand2 小时前
Element-ui的使用教程 基于HBuilder X
前端·javascript·vue.js·elementui
GDAL2 小时前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js
z千鑫2 小时前
【前端】详解前端三大主流框架:React、Vue与Angular的比较与选择
前端·vue.js·react.js
苹果醋34 小时前
React系列(八)——React进阶知识点拓展
运维·vue.js·spring boot·nginx·课程设计
王小王和他的小伙伴4 小时前
解决 vue3 中 echarts图表在el-dialog中显示问题
javascript·vue.js·echarts
好名字08215 小时前
前端取Content-Disposition中的filename字段与解码(vue)
前端·javascript·vue.js·前端框架
隐形喷火龙5 小时前
element ui--下拉根据拼音首字母过滤
前端·vue.js·ui
Simaoya7 小时前
【vue】css模拟玻璃球体效果(带高光)
前端·css·vue.js
ekskef_sef7 小时前
前端Vue框架基础介绍
前端·javascript·vue.js