手写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 相似的开发体验,同时保持了以下优势:

  • 更小的体积(仅核心功能)
  • 可定制的测量模式
  • 明确的类型定义
  • 更好的浏览器兼容性处理
相关推荐
橙子家2 分钟前
浏览器缓存之【结构化数据库与缓存】: IndexedDB、Cache storage 和 Storage buckets
前端
user20585561518137 分钟前
X6 中边悬浮置顶,规避 `mouseleave` 事件丢失问题
前端
李明卫杭州9 分钟前
CSS aspect-ratio 属性完全指南
前端
Pedantic2 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘2 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆2 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师3 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆4 小时前
VSCode自动格式化三要素
前端
爱勇宝4 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员