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

  • 更小的体积(仅核心功能)
  • 可定制的测量模式
  • 明确的类型定义
  • 更好的浏览器兼容性处理
相关推荐
止观止10 分钟前
深入探索 pnpm:高效磁盘利用与灵活的包管理解决方案
前端·pnpm·前端工程化·包管理器
whale fall11 分钟前
npm install安装的node_modules是什么
前端·npm·node.js
烛阴16 分钟前
简单入门Python装饰器
前端·python
袁煦丞1 小时前
数据库设计神器DrawDB:cpolar内网穿透实验室第595个成功挑战
前端·程序员·远程工作
天天扭码1 小时前
从图片到语音:我是如何用两大模型API打造沉浸式英语学习工具的
前端·人工智能·github
鱼樱前端2 小时前
今天介绍下最新更新的Vite7
前端·vue.js
coder_pig3 小时前
跟🤡杰哥一起学Flutter (三十四、玩转Flutter手势✋)
前端·flutter·harmonyos
万少3 小时前
01-自然壁纸实战教程-免费开放啦
前端
独立开阀者_FwtCoder3 小时前
【Augment】 Augment技巧之 Rewrite Prompt(重写提示) 有神奇的魔法
前端·javascript·github
yuki_uix4 小时前
AI辅助网页设计:从图片到代码的实践探索
前端