Vue3 企业级封装:useEventListener + 终极版 BaseEcharts 组件
前言
在企业级中后台项目里,ECharts 是使用频率极高的数据可视化库。但原生直接使用往往存在诸多问题:重复代码多、窗口自适应要手写监听、组件销毁容易内存泄漏、事件绑定繁琐、切换 Tab 图表空白等。
本文基于 Vue3 + 组合式 API,封装两个生产可用的工具:
- useEventListener:统一管理 DOM 事件,自动绑定、自动移除,杜绝内存泄漏
- BaseEcharts:支持统一事件派发、自适应、主题切换、loading、实例暴露,真正做到一次封装、全项目复用
文章最后附带高频面试题,无论是开发还是面试都非常实用。
一、封装通用事件监听 Hook:useEventListener
在开发中我们经常要监听 resize、scroll、click、keydown 等事件。
如果每次都手动 addEventListener + removeEventListener,代码冗余且极易忘记解绑,导致内存泄漏。
因此我们封装一个自动生命周期管理的 Hook。
useEventListener.js
javascript
import { onMounted, onUnmounted, watch, unref } from 'vue'
export function useEventListener(
eventName,
handler,
target = window,
options = {}
) {
const getTargetEl = () => unref(target)
function add() {
const el = getTargetEl()
el?.addEventListener?.(eventName, handler, options)
}
function remove() {
const el = getTargetEl()
el?.removeEventListener?.(eventName, handler, options)
}
onMounted(add)
onUnmounted(remove)
watch(() => getTargetEl(), (newEl, oldEl) => {
oldEl && remove()
newEl && add()
})
return { add, remove }
}
核心优势
- 自动在挂载时绑定、卸载时移除事件
- 支持
window/refDOM / 普通元素 - target 变化可自动重新绑定
- 彻底避免内存泄漏
二、终极封装 BaseEcharts 组件
功能亮点
- ✅ 支持
option响应式自动更新 - ✅ 统一派发所有 ECharts 事件 (通配符
*监听) - ✅ 自带窗口自适应(带防抖)
- ✅ 支持
light/dark主题切换 - ✅ 暴露
showLoading/resize/dispatchAction等方法 - ✅ 组件销毁自动
dispose,安全无内存泄漏 - ✅ 高度可配置、支持
v-if切换
BaseEcharts.vue
vue
<template>
<div ref="chartEl" class="base-echarts" :style="{ height }" />
</template>
<script setup>
import { ref, shallowRef, watch, onMounted, onUnmounted, defineExpose } from 'vue'
import * as echarts from 'echarts'
import { useEventListener } from '@/hooks/useEventListener'
const props = defineProps({
option: {
type: Object,
required: true
},
height: {
type: [String, Number],
default: '360px'
},
autoResize: {
type: Boolean,
default: true
},
theme: {
type: String,
default: ''
}
})
const emit = defineEmits(['echarts-event'])
const chartEl = ref(null)
const chartInstance = shallowRef(null)
// 初始化图表
function initChart() {
if (!chartEl.value) return
dispose()
chartInstance.value = echarts.init(chartEl.value, props.theme)
chartInstance.value.setOption(props.option)
bindAllEvents()
}
// 统一监听所有 ECharts 事件并抛出
function bindAllEvents() {
const chart = chartInstance.value
if (!chart) return
chart.on('*', (eventName, params) => {
emit('echarts-event', eventName, params)
})
}
// 配置更新
watch(
() => props.option,
(newOpt) => {
chartInstance.value?.setOption(newOpt)
},
{ deep: true }
)
// 主题变化重建
watch(() => props.theme, initChart)
// 防抖 resize
let resizeTimer = null
function handleResize() {
clearTimeout(resizeTimer)
resizeTimer = setTimeout(() => {
chartInstance.value?.resize()
}, 100)
}
// 监听窗口变化
if (props.autoResize) {
useEventListener('resize', handleResize)
}
// 销毁实例(关键)
function dispose() {
if (chartInstance.value) {
chartInstance.value.dispose()
chartInstance.value = null
}
}
// 暴露方法
defineExpose({
chart: chartInstance,
reload: initChart,
resize: handleResize,
showLoading: () => chartInstance.value?.showLoading(),
hideLoading: () => chartInstance.value?.hideLoading(),
clear: () => chartInstance.value?.clear(),
dispatchAction: (action) => chartInstance.value?.dispatchAction(action)
})
onMounted(initChart)
onUnmounted(dispose)
</script>
<style scoped>
.base-echarts {
width: 100%;
}
</style>
三、组件销毁时为什么必须调用 dispose?(重点)
在 Vue 中使用 ECharts 有一个非常关键的知识点:
组件销毁 ≠ 图表实例销毁
ECharts 是独立的第三方库实例,内部持有:
- DOM 引用
- 大量事件监听(click / hover / legend 等)
- 动画帧、定时器
- 数据集、配置缓存
这些资源Vue 不会自动回收 ,必须手动调用 dispose()。
如果不 dispose 会出现什么问题?
-
内存泄漏
实例驻留内存,路由切换多次后内存暴涨,页面越来越卡,最终崩溃。
-
事件与动画继续后台运行
占用 JS 主线程,导致页面掉帧、卡顿。
-
操作已销毁 DOM 报错
控制台出现大量
null引用异常。 -
实例堆积,引发渲染异常
图表重叠、不刷新、自适应失效等诡异问题。
dispose 做了什么?
- 销毁图表实例,释放内存
- 解绑所有 ZRender 事件
- 清除动画与定时器
- 断开 DOM 引用,使浏览器可以正常 GC
一句话总结:
Vue 只管理自己的生命周期,不管第三方实例。
不 dispose 的 ECharts,就是内存泄漏定时炸弹。
四、使用示例
vue
<template>
<BaseEcharts
:option="option"
height="400px"
theme="dark"
@echarts-event="handleEvent"
/>
</template>
<script setup>
import BaseEcharts from '@/components/BaseEcharts.vue'
const option = {
xAxis: { data: ['周一', '周二', '周三', '周四'] },
yAxis: {},
series: [{ type: 'bar', data: [12, 31, 25, 49] }]
}
function handleEvent(eventName, params) {
console.log(eventName, params)
if (eventName === 'click') {
// 处理点击
}
}
</script>
五、高频面试题
1. 为什么要封装 useEventListener?
- 简化事件绑定/解绑逻辑
- 防止忘记 remove 导致内存泄漏
- 统一管理,提高可维护性
- 支持响应式 target 自动重绑
2. ECharts 为什么要用 shallowRef?
ECharts 实例结构复杂,不需要响应式。
ref 会深度代理造成性能浪费,shallowRef 只代理顶层,性能更高。
3. 自适应为什么加防抖?
resize 触发极频繁,不加防抖会导致图表频繁重绘卡顿。
4. 组件销毁为什么必须 dispose?
ECharts 不属于 Vue 管理,不销毁会造成:
内存泄漏、事件残留、动画持续运行、控制台报错。
5. 统一事件派发的好处是什么?
一个 @echarts-event 接收所有事件,无需逐个绑定,扩展性极强,ECharts 新增事件自动支持。
6. 什么是内存泄漏?如何避免?
无用资源驻留内存无法释放就是内存泄漏。
避免方式:及时 dispose、清除事件监听、定时器、DOM 引用。
六、总结
本文封装的 useEventListener 和 BaseEcharts 是企业级中后台项目的标准实践:
- 事件自动管理,无内存泄漏
- 图表组件高度通用,支持所有业务场景
- 统一事件派发,灵活易用
- 自带自适应、主题、loading、实例方法
- 代码简洁、可维护性强、性能优秀
这套方案可以直接用于生产环境,大幅提升开发效率与项目稳定性。