背景
相信大家在日常的开发过程中大概率都遇到过需要监听Element
尺寸变化的情况。比较常见的就是Echarts
统计图了,当窗口变化大小的时候需要根据情况调整统计图的大小。下面的内容将以统计图为例来聊一聊我的处理方式。
addEventListener('resize', () => {})
一开始做这类需要监听尺寸变化的需要的时候都会使用这种方式,在组件实例挂载完成后使用addEventListener('resize', () => {})
来监听页面窗口的变化,然后在监听事件函数中调用Echart
的resize
方法,在组件实例被卸载之前调用removeEventListener('resize', () => {})
删除侦听器。附上代码:
<template>
<div class="container" ref="refContainer">
<div class="echart" ref="refEchart" />
</div>
</template>
<script setup>
import { ref, reactive, markRaw, onMounted } from 'vue'
import * as echarts from 'echarts'
const refContainer = ref()
const refEchart = ref()
const echart = ref(null)
const list = ref([])
const option = reactive({})
const init = () => {
// 初始化统计图
}
const resizeHandle = () => {
if (echart.value) {
echart.value.resize()
}
}
onMounted(() => {
init()
refContainer.value.addEventListener('resize', resizeHandle)
})
onBeforeUnmount(() => {
refContainer.value.removeEventListener('resize', resizeHandle)
})
</script>
<style scoped>
/* 省略样式 */
</style>
ResizeObserver
除了addEventListener
方式外还以一种方式就是使用ResizeObserver
。ResizeObserver
接口会监视Element
内容盒或边框盒或者SVGElement
边界尺寸的变化。换句话来说就是可以监听到DOM
元素宽高的变化。
内容盒是盒模型放置内容的部分,这意味着边框盒减去内边距和边框的宽度就是内容盒。边框盒包含内容、内边距和边框。
使用new ResizeObserver()
会创建并返回一个新的ResizeObserver
对象。它会有一个回调函数作为参数,每当观测的元素调整大小时,会调用该函数。这个回调函数会有两个参数:第一个参数是ResizeObserverEntry
对象数组可以用于获取每个元素改变后的新尺寸。第二个参数是对ResizeObserver
自身的引用。
ResizeObserverEntry
有五个属性:
-
borderBoxSize
,是一个数组,数组里是新边框盒大小的对象包含两个属性blockSize
:被监听的元素在块方向上的长度、inlineSize
:被监听的元素在内联方向上的长度。 -
contentBoxSize
,是一个数组,数组里是新内容盒大小的对象包含两个属性blockSize
:被监听的元素在块方向上的长度、inlineSize
:被监听的元素在内联方向上的长度。 -
contentRect
,一个DOMRectReadOnly
对象 -
devicePixelContentBoxSize
,是一个数组,数组里是被监听元素的设备像素大小的对象。 -
target
,是正在观察Element
的引用。
ResizeObserver
有三个方法:observe
开始对指定Element
的监听、unobserve()
结束对指定Element
的监听、disconnect()
取消特定观察者目标上所有对Element
的监听。
下面用代码简单实现一下:
<template>
<div class="container" ref="refContainer">
<div class="echart" ref="refEchart" />
</div>
</template>
<script setup>
import { ref, reactive, markRaw, onMounted } from 'vue'
import * as echarts from 'echarts'
const refContainer = ref()
const resizeObserver = ref(null)
const refEchart = ref()
const echart = ref(null)
const list = ref([])
const option = reactive({})
const init = () => {
// 初始化统计图
}
const resizeHandle = () => {
if (echart.value) {
echart.value.resize()
}
}
onMounted(() => {
init()
resizeObserver.value = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target === refContainer.value) {
resizeHandle()
}
}
})
resizeObserver.value.observe(refContainer.value)
})
onBeforeUnmount(() => {
resizeObserver.value.unobserve(refContainer.value)
})
</script>
<style scoped>
/* 省略... */
</style>
基于ResizeObserver实现自定义指令
上面两种方式都是实现需求,但是都有相同的一个弊端:如果有很多组件里面都需要监听,那所有的组件都要写这些重复的代码。显然这样并不是我们所希望的,有没有什么方式可以不用这样做呢?当然是有。下面我就带领大家使用自定义指令的方式基于ResizeObserver
来实现这个需求。
由上文介绍的ResizeObserver
中知道它的回调函数返回的是一个数组,那我们就可以得出它是可以监听多个Element
的。在这个基础上我们来实现一下:
const weakMap = new WeakMap()
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const handler = weakMap.get(entry.target)
if (handler) {
handler()
}
}
})
export default {
mounted(el, binding) {
resizeObserver.observe(el)
weakMap.set(el, binding.value)
},
unmounted(el) {
resizeObserver.unobserve(el)
}
}
以上代码就可以实现需求,但是并不能满足所有场景,比如有些场景可能需要Element
的尺寸,我们在这个基础上优化一下。完整代码:
// resize.js
const args = ['borderBoxSize', 'contentBoxSize', 'contentRect', 'devicePixelContentBoxSize']
const weakMap = new WeakMap()
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const value = weakMap.get(entry.target)
if (value) {
let { arg, handler } = value
arg = arg || args[1]
let params = entry[arg]
if (arg !== args[2]) {
const { blockSize, inlineSize } = entry[arg][0]
params = {
height: blockSize,
width: inlineSize
}
}
handler(params)
}
}
})
export default {
mounted(el, binding) {
const { arg, value } = binding
if (typeof value !== 'function' ) {
console.warn('[Directive warn]: Invalid value: validation failed for value. Must be a function.')
return
}
if (arg && !args.includes(arg) ) {
console.warn(`[Directive warn]: Invalid arg: validation failed for arg. Expected one of ${ JSON.stringify(args) }, got value "${arg}".`)
return
}
resizeObserver.observe(el)
weakMap.set(el, {arg, handler:binding.value})
},
unmounted(el) {
resizeObserver.unobserve(el)
}
}
// index.vue
<template>
<div class="container" v-resize="resizeHandle">
<div class="echart" ref="refEchart" />
</div>
</template>
<script setup>
// 省略...
</script>
<style scoped>
/* 省略... */
</style>
这里使用WeakMap是它的键是弱引用的,其键必须是对象,而值可以是任意的。它的键被弱保持,也就是说,当其键所指对象没有其他地方引用的时候,它会被 GC 回收掉。
以上就是本次分享的内容,附上源码。
感谢看官看到这里,如果觉得文章不错的话,可以给小生的几个开源项目点个Star⭐!
- 基于 Vue3 + Element-plus 管理后台基础功能框架
- 基于 Vue3 + Element-plus + websocket 即时聊天系统
- 基于 node 开发的后端服务:https://github.com/gmingchen/node-server