如何基于 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 库,可以来看看,没事还可以点个赞~~~谢谢~~~

相关推荐
你挚爱的强哥1 小时前
✅✅✅【Vue.js】sd.js基于jQuery Ajax最新原生完整版for凯哥API版本
javascript·vue.js·jquery
susu10830189112 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
天天进步20154 小时前
Vue+Springboot用Websocket实现协同编辑
vue.js·spring boot·websocket
疯狂的沙粒5 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员5 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
想自律的露西西★6 小时前
用el-scrollbar实现滚动条,拖动滚动条可以滚动,但是通过鼠标滑轮却无效
前端·javascript·css·vue.js·elementui·前端框架·html5
白墨阳6 小时前
vue3:瀑布流
前端·javascript·vue.js
程序媛-徐师姐7 小时前
Java 基于SpringBoot+vue框架的老年医疗保健网站
java·vue.js·spring boot·老年医疗保健·老年 医疗保健
余道各努力,千里自同风8 小时前
前端 vue 如何区分开发环境
前端·javascript·vue.js
PandaCave8 小时前
vue工程运行、构建、引用环境参数学习记录
javascript·vue.js·学习