博客 / 詳情

返回

如何基於 vue3.x 編寫自己的 hook

什麼是 hooks

函數式編程在前端開發中越來越流行,尤其是在現代前端框架 Vue3.xReact 16+ 中。它的優點包括代碼可讀性、可維護性、可測試性和複用性。

學習如何利用框架提供的鈎子(hooks)編寫自定義鈎子函數是非常重要的技能之一。通過編寫自定義鈎子函數,我們可以滿足特定需求,使我們的代碼更加靈活和可擴展。

掌握函數式編程和鈎子的使用,能夠提高我們的開發效率,同時提供更好的用户體驗和代碼質量。在使用 Vue3.xReact 16+ 等現代前端框架時,函數式編程和自定義鈎子函數將成為我們必備的技能。

優秀的 vue3.x hooks 庫

  • vue-hooks-plus
  • vueuse

兩個出色的開源庫,它們幾乎可以滿足各種場景的需求。然而,學習如何獨立編寫代碼也是非常重要的。通過親自動手編寫代碼,我們能夠深入理解底層原理,培養自己的解決問題的能力。

起步

假設你已經瞭解並且掌握 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 庫,可以來看看,沒事還可以點個贊~~~謝謝~~~

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.