Vue 3中通过ref获取元素高度及尺寸监听方案

15 次阅读

一、Vue 3中使用ref获取元素高度的基本方法

Vue 3的Composition API提供了ref、onMounted等工具,可以比较方便地获取DOM元素的实际尺寸。对于需要根据容器高度调整布局、计算可视区域、实现自适应组件等场景,使用ref获取元素尺寸是非常常见的做法。

模板中先通过ref绑定目标元素:

vue

   ref="boxRef" class="box">
    内容区域
  
setup> import { ref, onMounted } from 'vue' const boxRef = ref(null) onMounted(() => { console.log('元素高度:', boxRef.value.offsetHeight) })

这里的boxRef初始值为null,因为组件尚未挂载到DOM时,元素并不存在。只有进入onMounted生命周期之后,才能通过boxRef.value访问真实DOM节点。

获取高度时常见的属性主要有以下几种:

JavaScript
boxRef.value.offsetHeight
boxRef.value.clientHeight
boxRef.value.scrollHeight

它们的含义并不完全相同。

offsetHeight通常表示元素的布局高度,包括内容、内边距、边框,但不包含外边距。

clientHeight表示元素的内部可用高度,通常包含padding,但不包含边框和滚动条。

scrollHeight则表示元素内容实际需要的完整高度,即使部分内容因为滚动区域而没有显示,也会计算在内。

因此,如果只是想获取一个普通容器的可见布局高度,offsetHeight通常比较直观。


二、使用getBoundingClientRect获取元素完整尺寸

如果不仅需要高度,还需要元素的宽度、位置等信息,可以使用getBoundingClientRect()。

vue

   ref="boxRef" class="box">
    内容区域
  
setup> import { ref, onMounted } from 'vue' const boxRef = ref(null) onMounted(() => { const rect = boxRef.value.getBoundingClientRect() console.log('宽度:', rect.width) console.log('高度:', rect.height) console.log('顶部位置:', rect.top) console.log('左侧位置:', rect.left) })

返回的DOMRect包含多个有用属性:

width
height
top
right
bottom
left
x
y

与offsetHeight相比,getBoundingClientRect()更适合处理需要同时获取宽高和页面位置的场景。

例如,一个弹窗需要定位到某个按钮附近,或者一个浮层需要根据目标元素的位置动态计算坐标,就可以直接使用:

JavaScript
const rect = boxRef.value.getBoundingClientRect()

然后结合rect.top、rect.left等数据进行计算。


三、为什么不能直接在setup阶段获取元素高度

很多Vue 3初学者会写出类似代码:

JavaScript
const boxRef = ref(null)

console.log(boxRef.value.offsetHeight)

这种方式通常无法得到目标元素的尺寸,因为setup()执行时模板对应的DOM节点还没有完成挂载。

正确做法是放到onMounted()中:

JavaScript
import { ref, onMounted } from 'vue'

const boxRef = ref(null)

onMounted(() => {
  const height = boxRef.value.offsetHeight
  console.log(height)
})

如果元素的高度还依赖异步数据、条件渲染或者响应式状态,那么即使进入onMounted(),也不一定代表最终尺寸已经确定。

例如:

vue

   ref="boxRef">
     v-if="showContent">
      动态内容
    

如果后续修改:

JavaScript
showContent.value = true

DOM需要经过Vue的更新周期才会真正发生变化。这种情况下可以配合nextTick():

JavaScript
import { nextTick } from 'vue'

showContent.value = true

await nextTick()

console.log(boxRef.value.offsetHeight)

nextTick()解决的是“等待Vue完成DOM更新”的问题,但它并不能替代尺寸监听。


四、使用ResizeObserver监听元素高度变化

如果元素高度会随着窗口变化、文字换行、异步内容加载、子元素变化等情况不断改变,仅仅在onMounted()获取一次高度是不够的。

这时更适合使用浏览器原生的ResizeObserver。

vue

   ref="boxRef" class="box">
    动态内容
  

当前高度:{{ height }}px

setup> import { ref, onMounted, onBeforeUnmount } from 'vue' const boxRef = ref(null) const height = ref(0) let observer = null onMounted(() => { observer = new ResizeObserver((entries) => { const entry = entries[0] height.value = entry.contentRect.height }) observer.observe(boxRef.value) }) onBeforeUnmount(() => { observer?.disconnect() })

这样,当目标元素尺寸发生变化时,回调函数就会被触发。

ResizeObserver相比监听window.resize更加精准,因为它关注的是指定元素本身,而不是整个浏览器窗口。


五、ResizeObserver与window.resize有什么区别

很多项目会使用下面这种方式监听尺寸:

JavaScript
window.addEventListener('resize', handleResize)

然后在回调中重新获取:

JavaScript
const height = boxRef.value.offsetHeight

这种方式可以处理浏览器窗口尺寸变化,但存在明显局限。

例如,一个容器可能因为以下原因改变高度:

这些变化并不一定伴随window.resize事件。

而ResizeObserver直接观察元素自身尺寸:

JavaScript
observer.observe(boxRef.value)

因此,对于“监听Vue组件内部某个元素高度变化”这一需求,通常应该优先考虑ResizeObserver。


六、同时监听元素宽度和高度

ResizeObserver并不只适合获取高度。

JavaScript
const observer = new ResizeObserver((entries) => {
  const entry = entries[0]

  const width = entry.contentRect.width
  const height = entry.contentRect.height

  console.log('宽度:', width)
  console.log('高度:', height)
})

也可以将尺寸保存到Vue响应式变量:

JavaScript
const size = ref({
  width: 0,
  height: 0
})

const observer = new ResizeObserver(([entry]) => {
  size.value = {
    width: entry.contentRect.width,
    height: entry.contentRect.height
  }
})

模板中即可直接使用:

vue

宽度:{{ size.width }}px

高度:{{ size.height }}px

这种写法适合开发通用的自适应组件。


七、封装成Vue 3可复用组合式函数

如果项目中多个组件都需要监听元素尺寸,可以将逻辑封装成Composable,避免重复编写ResizeObserver代码。

例如:

JavaScript
import { ref, onMounted, onBeforeUnmount } from 'vue'

export function useElementSize(elementRef) {
  const width = ref(0)
  const height = ref(0)

  let observer = null

  onMounted(() => {
    if (!elementRef.value) return

    const updateSize = (element) => {
      width.value = element.contentRect.width
      height.value = element.contentRect.height
    }

    observer = new ResizeObserver(([entry]) => {
      updateSize(entry)
    })

    observer.observe(elementRef.value)
  })

  onBeforeUnmount(() => {
    observer?.disconnect()
  })

  return {
    width,
    height
  }
}

组件中使用:

vue
 setup>
import { ref } from 'vue'
import { useElementSize } from './useElementSize'

const boxRef = ref(null)

const { width, height } = useElementSize(boxRef)



   ref="boxRef">
    内容区域
  
宽度:{{ width }}px,高度:{{ height }}px

这样可以将DOM尺寸监听逻辑集中管理,使业务组件更加简洁。


八、获取v-for循环元素的尺寸

ref还可以用于获取v-for生成的多个DOM元素。

例如:

vue

  
    v-for="item in list"
    :key="item.id"
    ref="itemRefs"
  >
    {{ item.name }}
  
setup> import { ref, onMounted } from 'vue' const list = ref([ { id: 1, name: '内容一' }, { id: 2, name: '内容二' }, { id: 3, name: '内容三' } ]) const itemRefs = ref([]) onMounted(() => { itemRefs.value.forEach((element) => { console.log(element.offsetHeight) }) })

当同一个ref用于v-for元素时,Vue会收集对应的DOM节点。

需要注意的是,循环数据发生变化时,元素数组也可能随之变化。如果业务逻辑对元素顺序、尺寸对应关系要求较高,应结合key以及实际DOM结构进行处理,不要假设DOM数组永远与业务数据存在完全固定的对应关系。


九、获取组件内部元素尺寸

如果ref绑定的是组件而不是原生DOM元素:

vue

   ref="childRef" />

此时:

JavaScript
const childRef = ref(null)

childRef.value通常指向的是组件实例,而不是直接的HTML元素。

如果需要获取子组件内部DOM元素,可以在子组件内部主动暴露方法或数据。

例如子组件:

vue
 setup>
import { ref } from 'vue'

const boxRef = ref(null)

function getHeight() {
  return boxRef.value?.offsetHeight ?? 0
}

defineExpose({
  getHeight
})



   ref="boxRef">
    子组件内容
  

父组件:

vue
 setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'

const childRef = ref(null)

onMounted(() => {
  console.log(childRef.value.getHeight())
})



   ref="childRef" />

这种方式比直接依赖子组件内部DOM结构更加清晰,也能降低父子组件之间的耦合。


十、获取高度时需要注意CSS盒模型

元素尺寸获取结果与CSS盒模型密切相关。

例如:

CSS
.box {
  width: 300px;
  padding: 20px;
  border: 2px solid #ddd;
}

此时不同API得到的尺寸可能不同。

可以简单理解为:

offsetHeight
≈ content + padding + border

clientHeight
≈ content + padding

scrollHeight
≈ 内容实际需要的高度 + padding

而:

JavaScript
element.getBoundingClientRect().height

返回的是元素当前布局矩形的高度,实际开发中非常适合用于精确布局计算。

因此,不应该看到“获取高度”就固定使用某一个属性,而应该根据业务目的选择。


十一、元素高度变化时避免频繁执行复杂逻辑

ResizeObserver能够持续触发回调。如果监听的元素尺寸变化非常频繁,而回调中又包含大量计算,就可能增加页面开销。

例如:

JavaScript
const observer = new ResizeObserver(() => {
  // 大量复杂计算
  calculateLayout()
  updateChart()
  refreshData()
})

对于复杂场景,可以考虑减少无意义的重复处理,例如记录上一次尺寸:

JavaScript
let lastWidth = 0
let lastHeight = 0

const observer = new ResizeObserver(([entry]) => {
  const width = entry.contentRect.width
  const height = entry.contentRect.height

  if (width === lastWidth && height === lastHeight) {
    return
  }

  lastWidth = width
  lastHeight = height

  console.log('尺寸发生变化', width, height)
})

如果回调中涉及大量DOM操作、图表重绘或者复杂计算,还可以根据实际场景配合节流、防抖或requestAnimationFrame进行优化。


十二、组件销毁时记得清理ResizeObserver

使用ResizeObserver时,一个容易被忽略的问题是生命周期管理。

推荐:

JavaScript
onMounted(() => {
  observer = new ResizeObserver(...)
  observer.observe(boxRef.value)
})

onBeforeUnmount(() => {
  observer?.disconnect()
})

组件销毁后主动调用:

JavaScript
observer.disconnect()

可以停止观察,避免无意义的监听关系继续存在。

如果只监听单个元素,也可以使用:

JavaScript
observer.unobserve(boxRef.value)

具体选择disconnect()还是unobserve(),取决于一个Observer实例是否同时负责多个元素。


十三、SSR环境下使用ref获取尺寸的注意事项

如果Vue 3项目使用服务端渲染,例如Nuxt等框架,需要特别注意DOM API只能在客户端环境使用。

以下代码不能直接在服务端执行:

JavaScript
document.querySelector('.box')
window.innerWidth
new ResizeObserver(...)

通常应该把依赖浏览器DOM的逻辑放入客户端生命周期中:

JavaScript
onMounted(() => {
  // 浏览器端执行
})

因为服务端渲染阶段没有真实浏览器DOM,自然也无法获得元素的实际高度。


十四、Vue 3获取元素高度的常见错误

1. ref没有绑定到元素

JavaScript
const boxRef = ref(null)

仅仅声明ref还不够,模板必须存在:

vue
 ref="boxRef">

2. 在DOM创建之前读取尺寸

错误:

JavaScript
console.log(boxRef.value?.offsetHeight)

如果执行时组件尚未挂载,得到的可能是null或无法获取有效尺寸。

应使用:

JavaScript
onMounted(() => {
  console.log(boxRef.value.offsetHeight)
})

3. 修改数据后立即读取DOM

例如:

JavaScript
show.value = true

console.log(boxRef.value.offsetHeight)

此时Vue可能还没有完成DOM更新。

可以使用:

JavaScript
show.value = true

await nextTick()

console.log(boxRef.value.offsetHeight)

4. 用window.resize代替元素尺寸监听

如果真正需要监听的是某个容器,而不是浏览器窗口,那么单纯使用:

JavaScript
window.addEventListener('resize', ...)

并不全面。

更适合使用:

JavaScript
new ResizeObserver(...)

5. 忽略组件卸载后的清理

长期运行的页面尤其需要注意监听器清理,否则可能产生不必要的资源占用。


十五、实际项目中的推荐实现

对于普通场景,只需要组件挂载后获取一次高度:

JavaScript
const boxRef = ref(null)

onMounted(() => {
  const height = boxRef.value?.offsetHeight ?? 0
})

如果DOM更新后需要重新计算:

JavaScript
await nextTick()

const height = boxRef.value?.offsetHeight ?? 0

如果要求持续监听元素尺寸:

JavaScript
const observer = new ResizeObserver(([entry]) => {
  const height = entry.contentRect.height
})

observer.observe(boxRef.value)

如果项目中多个地方都有相同需求,则建议进一步封装为useElementSize等组合式函数。

从实际开发角度来看,可以按照需求进行选择:一次性读取使用ref + onMounted,DOM更新后读取使用nextTick,持续监听尺寸变化优先使用ResizeObserver。这种划分既简单又符合Vue 3的响应式开发方式。