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

  • 更小的体积(仅核心功能)
  • 可定制的测量模式
  • 明确的类型定义
  • 更好的浏览器兼容性处理
相关推荐
冴羽4 分钟前
SvelteKit 最新中文文档教程(3)—— 数据加载
前端·javascript·svelte
百万蹄蹄向前冲21 分钟前
组建百万前端梦之队-计算机大学生竞赛发展蓝图
前端·vue.js·掘金社区
云隙阳光i34 分钟前
实现手机手势签字功能
前端·javascript·vue.js
imkaifan1 小时前
vue2升级Vue3--native、对inheritAttrs作用做以解释、声明的prop属性和未声明prop的属性
前端·vue.js·native修饰符·inheritattrs作用·声明的prop属性·未声明prop的属性
觉醒法师1 小时前
HarmonyOS NEXT - 电商App实例三( 网络请求axios)
前端·华为·typescript·axios·harmonyos·ark-ts
Danta1 小时前
HTTP协议版本演进:从HTTP/0.9到HTTP/3的高分面试回答
前端·网络协议·面试
柠檬树^-^2 小时前
app.config.globalProperties
前端·javascript·vue.js
太阳花ˉ2 小时前
react(一):特点-基本使用-JSX语法
前端·react.js
赵大仁2 小时前
深入解析 React Diff 算法:原理、优化与实践
前端·react.js·前端框架
1024小神2 小时前
vue/react/vite前端项目打包的时候加上时间最简单版本,防止后端扯皮
前端·vue.js·react.js