手写VueUse的useElementSize函数实现

以下是一个手写实现的 useElementSize 函数,用于监听元素尺寸变化并返回响应式尺寸数据:

typescript 复制代码
import {
  ref,
  watch,
  onUnmounted,
  unref,
  type Ref,
  type ComponentPublicInstance
} from 'vue'

interface UseElementSizeOptions {
  initialSize?: { width?: number; height?: number }
  box?: 'content-box' | 'border-box'
}

export function useElementSize(
  target: Ref<HTMLElement | ComponentPublicInstance | null>,
  options: UseElementSizeOptions = {}
) {
  const { initialSize = {}, box = 'content-box' } = options
  const width = ref(initialSize.width ?? 0)
  const height = ref(initialSize.height ?? 0)
  let observer: ResizeObserver | null = null

  const getElement = (): HTMLElement | null => {
    const el = unref(target)
    return (el as ComponentPublicInstance)?.$el ?? el
  }

  const updateSize = (entries: ResizeObserverEntry[]) => {
    const entry = entries[0]
    if (!entry) return

    const boxSize = entry[box === 'border-box' ? 'borderBoxSize' : 'contentBoxSize']
    const hasBorderBoxSize = box === 'border-box' && boxSize
    
    if (hasBorderBoxSize) {
      width.value = boxSize[0].inlineSize
      height.value = boxSize[0].blockSize
    } else {
      width.value = entry.contentRect.width
      height.value = entry.contentRect.height
    }
  }

  const observeElement = (el: HTMLElement | null) => {
    if (!el) return
    
    observer = new ResizeObserver(updateSize)
    observer.observe(el, { box: box === 'border-box' ? 'border-box' : 'content-box' })
  }

  const disconnectObserver = () => {
    if (observer) {
      observer.disconnect()
      observer = null
    }
  }

  watch(
    () => getElement(),
    (newEl, oldEl, onCleanup) => {
      disconnectObserver()
      observeElement(newEl)
      
      onCleanup(() => {
        disconnectObserver()
      })
    },
    { immediate: true }
  )

  const stop = () => {
    disconnectObserver()
  }

  onUnmounted(() => {
    disconnectObserver()
  })

  return {
    width,
    height,
    stop
  }
}

使用示例:

vue 复制代码
<template>
  <div ref="targetEl" :style="{ resize: 'both', overflow: 'auto' }">
    可调整大小的元素
  </div>
  <p>宽度: {{ width }}px</p>
  <p>高度: {{ height }}px</p>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useElementSize } from './useElementSize'

const targetEl = ref<HTMLElement | null>(null)

const { width, height, stop } = useElementSize(targetEl, {
  initialSize: { width: 200, height: 100 },
  box: 'border-box'
})

// 手动停止监听
// setTimeout(stop, 5000)
</script>

实现细节说明:

  1. 核心功能

    • 使用现代浏览器 ResizeObserver API 实现高性能尺寸监听
    • 支持 content-box 和 border-box 两种测量模式
    • 自动处理组件卸载时的资源清理
  2. 响应式数据

    • width: 响应式宽度值
    • height: 响应式高度值
    • 自动更新机制:当元素尺寸变化时,自动更新尺寸数据
  3. 高级特性

    • 支持初始尺寸设置 (initialSize)
    • 提供手动停止监听方法 (stop())
    • 自动处理 Vue 组件实例的 DOM 元素获取
  4. 性能优化

    • 自动断开旧元素的监听
    • 使用 ResizeObserver 的现代浏览器特性
    • 自动清理观察器实例
  5. 类型安全

    • 完整 TypeScript 类型支持
    • 兼容 Vue 组件实例和原生 DOM 元素
    • 严格的参数类型校验

配置选项说明:

参数 类型 默认值 说明
initialSize { width?: number; height?: number } {} 初始尺寸值,用于元素挂载前的默认值
box 'content-box' | 'border-box' 'content-box' 测量模式: - content-box: 内容区域尺寸 - border-box: 包含边框尺寸

实现亮点:

  1. 智能元素获取

    typescript 复制代码
    const getElement = () => {
      const el = unref(target)
      return (el as ComponentPublicInstance)?.$el ?? el
    }

    自动处理 Vue 组件实例,获取其对应的 DOM 元素

  2. 尺寸计算优化

    typescript 复制代码
    const boxSize = entry[box === 'border-box' ? 'borderBoxSize' : 'contentBoxSize']

    根据配置选项使用不同的尺寸计算方式,提供更精确的尺寸数据

  3. 自动清理机制

    typescript 复制代码
    watch(() => getElement(), (newEl, oldEl, onCleanup) => {
      // ...
      onCleanup(() => disconnectObserver())
    })

    使用 watch 的清理回调函数确保元素切换时的资源释放

  4. 跨浏览器兼容

    typescript 复制代码
    const hasBorderBoxSize = box === 'border-box' && boxSize
    if (hasBorderBoxSize) {
      // 现代浏览器支持
    } else {
      // 兼容旧版本浏览器
    }

    同时支持现代浏览器的新特性和旧浏览器的 fallback 方案

使用场景建议:

  1. 响应式布局组件
  2. 图表/可视化组件
  3. 自适应内容区域
  4. 拖拽调整大小功能
  5. 媒体元素尺寸监控

该实现提供了与 VueUse 相似的开发体验,同时保持了以下优势:

  • 更小的体积(仅核心功能)
  • 可定制的测量模式
  • 明确的类型定义
  • 更好的浏览器兼容性处理
相关推荐
gnip2 小时前
企业级配置式表单组件封装
前端·javascript·vue.js
一只叫煤球的猫3 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
excel4 小时前
Three.js 材质(Material)详解 —— 区别、原理、场景与示例
前端
掘金安东尼4 小时前
抛弃自定义模态框:原生Dialog的实力
前端·javascript·github
hj5914_前端新手8 小时前
javascript基础- 函数中 this 指向、call、apply、bind
前端·javascript
薛定谔的算法8 小时前
低代码编辑器项目设计与实现:以JSON为核心的数据驱动架构
前端·react.js·前端框架
Hilaku8 小时前
都2025年了,我们还有必要为了兼容性,去写那么多polyfill吗?
前端·javascript·css
yangcode8 小时前
iOS 苹果内购 Storekit 2
前端
LuckySusu8 小时前
【js篇】JavaScript 原型修改 vs 重写:深入理解 constructor的指向问题
前端·javascript
LuckySusu8 小时前
【js篇】如何准确获取对象自身的属性?hasOwnProperty深度解析
前端·javascript