在 Vue3 中监听一个 div 元素的动态高度变化并同步设置另一个元素的高度,最佳实践是使用 ResizeObserver API。
在开发中,常有上中下分布列表页,如下:
需求现象 :列表滚动区域需要具体高度来滚动,但是搜索区域会随页面拉伸等变化高度。
处理方法一:最佳方案:使用 ResizeObserver
- ResizeObserver API:
现代浏览器原生支持,高效监听元素尺寸变化
比轮询或MutationObserver性能更好 - 生命周期管理:
onMounted中初始化观察器
onBeforeUnmount中清理观察器,避免内存泄漏
- 初始高度设置:
在开始观察前先同步一次初始高度 - 平滑过渡:
通过CSS transition实现高度变化的动画效果
<template>
<div class="container">
<!-- 高度会动态变化的源元素 -->
<div ref="sourceRef" class="source-element">
内容可能动态变化导致高度改变...
</div>
<!-- 需要同步高度的目标元素 -->
<div ref="targetRef" class="target-element">
我的高度会跟随上方元素
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
const sourceRef = ref(null)
const targetRef = ref(null)
let resizeObserver = null
// 处理高度变化的回调函数
const handleResize = (entries) => {
if (!targetRef.value) return
for (let entry of entries) {
const { height } = entry.contentRect
targetRef.value.style.height = `${height}px`
// 也可以使用CSS变量:
// targetRef.value.style.setProperty('--dynamic-height', `${height}px`)
}
}
onMounted(() => {
// 初始化观察器
resizeObserver = new ResizeObserver(handleResize)
if (sourceRef.value) {
// 开始观察源元素
resizeObserver.observe(sourceRef.value)
// 初始设置一次高度
const initHeight = sourceRef.value.getBoundingClientRect().height
targetRef.value.style.height = `${initHeight}px`
}
})
onBeforeUnmount(() => {
// 组件卸载时停止观察
if (resizeObserver) {
resizeObserver.disconnect()
}
// 或
// resizeObserver?.disconnect()
})
</script>
<style>
.source-element {
border: 1px solid #ccc;
padding: 20px;
/* 高度由内容决定 */
}
.target-element {
border: 1px solid #f0f;
margin-top: 10px;
overflow: hidden;
transition: height 0.3s ease; /* 添加平滑过渡效果 */
}
</style>
优化—响应式地存储高度值
这种方法将高度值存储在响应式变量中,便于在其他地方使用。
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
const sourceRef = ref(null)
const targetRef = ref(null)
const dynamicHeight = ref(0)
let resizeObserver = null
onMounted(() => {
resizeObserver = new ResizeObserver((entries) => {
entries.forEach(entry => {
dynamicHeight.value = entry.contentRect.height
})
})
if (sourceRef.value) {
resizeObserver.observe(sourceRef.value)
}
})
// 使用watch监听高度变化
watch(dynamicHeight, (newHeight) => {
if (targetRef.value) {
targetRef.value.style.height = `${newHeight}px`
}
})
onBeforeUnmount(() => {
resizeObserver?.disconnect()
})
</script>
使用 @resize 事件(需配合自定义指令)
如果项目中频繁需要监听尺寸变化,可以封装一个自定义指令:
// directives/resize.js
export default {
mounted(el, binding) {
const callback = binding.value;
const observer = new ResizeObserver((entries) => {
callback(entries[0].contentRect);
});
observer.observe(el);
el._resizeObserver = observer;
},
unmounted(el) {
if (el._resizeObserver) {
el._resizeObserver.disconnect();
}
},
};
<template>
<div v-resize="onSourceResize">源元素</div>
<div :style="{ height: targetHeight + 'px' }">目标元素</div>
</template>
<script setup>
import { ref } from 'vue';
import resizeDirective from './directives/resize';
const targetHeight = ref(0);
const onSourceResize = (rect) => {
targetHeight.value = rect.height;
};
</script>
注意事项:
性能:ResizeObserver 是异步触发的,避免在回调中执行高耗能操作。
兼容性:如果需要支持旧浏览器,需引入 resize-observer-polyfill。
初始高度:在 onMounted 中可能需要手动设置一次初始高度。
完整示例(基于 ResizeObserver)
<template>
<div ref="source" class="source">
<p>源元素内容(高度可能变化)</p>
<button @click="addContent">增加内容</button>
</div>
<div ref="target" class="target" :style="{ height: targetHeight + 'px' }">
目标元素(高度同步源元素)
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
const source = ref(null);
const target = ref(null);
const targetHeight = ref(0);
let observer = null;
const updateHeight = () => {
if (source.value) {
targetHeight.value = source.value.offsetHeight;
}
};
onMounted(() => {
observer = new ResizeObserver(updateHeight);
if (source.value) {
observer.observe(source.value);
updateHeight(); // 初始化高度
}
});
onBeforeUnmount(() => {
if (observer) observer.disconnect();
});
const addContent = () => {
const p = document.createElement('p');
p.textContent = '新增内容 ' + Math.random();
source.value.appendChild(p);
};
</script>
<style>
.source {
border: 1px solid blue;
padding: 10px;
margin-bottom: 10px;
}
.target {
border: 1px solid red;
padding: 10px;
background: #eee;
}
</style>