以下是一个手写实现的 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>
实现细节说明:
-
核心功能:
- 使用现代浏览器 ResizeObserver API 实现高性能尺寸监听
- 支持 content-box 和 border-box 两种测量模式
- 自动处理组件卸载时的资源清理
-
响应式数据:
width
: 响应式宽度值height
: 响应式高度值- 自动更新机制:当元素尺寸变化时,自动更新尺寸数据
-
高级特性:
- 支持初始尺寸设置 (
initialSize
) - 提供手动停止监听方法 (
stop()
) - 自动处理 Vue 组件实例的 DOM 元素获取
- 支持初始尺寸设置 (
-
性能优化:
- 自动断开旧元素的监听
- 使用 ResizeObserver 的现代浏览器特性
- 自动清理观察器实例
-
类型安全:
- 完整 TypeScript 类型支持
- 兼容 Vue 组件实例和原生 DOM 元素
- 严格的参数类型校验
配置选项说明:
参数 | 类型 | 默认值 | 说明 |
---|---|---|---|
initialSize | { width?: number; height?: number } | {} | 初始尺寸值,用于元素挂载前的默认值 |
box | 'content-box' | 'border-box' | 'content-box' | 测量模式: - content-box: 内容区域尺寸 - border-box: 包含边框尺寸 |
实现亮点:
-
智能元素获取:
typescriptconst getElement = () => { const el = unref(target) return (el as ComponentPublicInstance)?.$el ?? el }
自动处理 Vue 组件实例,获取其对应的 DOM 元素
-
尺寸计算优化:
typescriptconst boxSize = entry[box === 'border-box' ? 'borderBoxSize' : 'contentBoxSize']
根据配置选项使用不同的尺寸计算方式,提供更精确的尺寸数据
-
自动清理机制:
typescriptwatch(() => getElement(), (newEl, oldEl, onCleanup) => { // ... onCleanup(() => disconnectObserver()) })
使用 watch 的清理回调函数确保元素切换时的资源释放
-
跨浏览器兼容:
typescriptconst hasBorderBoxSize = box === 'border-box' && boxSize if (hasBorderBoxSize) { // 现代浏览器支持 } else { // 兼容旧版本浏览器 }
同时支持现代浏览器的新特性和旧浏览器的 fallback 方案
使用场景建议:
- 响应式布局组件
- 图表/可视化组件
- 自适应内容区域
- 拖拽调整大小功能
- 媒体元素尺寸监控
该实现提供了与 VueUse 相似的开发体验,同时保持了以下优势:
- 更小的体积(仅核心功能)
- 可定制的测量模式
- 明确的类型定义
- 更好的浏览器兼容性处理