Skip to content

大屏项目 vue3(二)

图表组件封装

将图表的初始化过程封装为一个组合函数useEcharts,最终返回一个 ref 给图表组件和 echarts 实例

ts
import * as echarts from "echarts";
import { onMounted, onUnmounted, ref, watch, type Ref } from "vue";

export const useEcharts = (options: Ref<echarts.EChartsCoreOption>) => {
  const chartRef = ref<HTMLDivElement | null>(null);
  let chartInstance: echarts.ECharts | null = null;

  const initChart = () => {
    if (chartRef.value) {
      chartInstance = echarts.init(chartRef.value);
      chartInstance.setOption(options.value);
    }
  };

  const resizeChart = () => {
    if (chartInstance) {
      chartInstance.resize();
    }
  };
  //监听options变化
  watch(
    () => options.value,
    (newVal) => {
      if (chartInstance) {
        chartInstance.setOption(newVal);
      }
    },
    { deep: true }
  );

  onMounted(() => {
    initChart();
    window.addEventListener("resize", resizeChart);
  });

  onUnmounted(() => {
    if (chartInstance) {
      chartInstance.dispose();
      chartInstance = null;
    }
    window.removeEventListener("resize", resizeChart);
  });
  return { chartRef, chartInstance };
};

内存泄漏优化

TIP

summary 模式:

  • Constructor:占用内存的资源类型
  • Distance: 当前对象到根的引用层级距离
  • Shallow Size: 对象所占内存(不包含内部引用的其它对象所占的内存)(单位:字节)
  • Retained Size: 对象所占总内存(包含内部引用的其它对象所占的内存)(单位:字节)

compare 模式:

  • New:新分配的内存空间数
  • Deleted:销毁的内存空间数
  • Delta:内存回收差值=新分配-销毁

背景:在客户现场运行了几天后页面突然崩溃了,本地短时间测试没有复现

排查:

  1. 先使用开发者工具的内存面板监控,发现在渲染大量图表时,页面内存占用不断上升,初步怀疑是 Echarts 图表渲染产生的内存泄漏,最终导致页面崩溃
  2. 基于这个猜测去排查了项目代码,发现确实有问题,代码里写了个定时器,每次刷新数据时,都会调用init,加上销毁时 clear 和 dispose 方法使用不当,定时器循环重绘图表导致内存一直升高,最终页面崩溃。

优化:

  1. 只调用一次echarts.init,通过 watch 合理使用 setOptions 更新图表内容,而不是每次都创建一个 echarts 实例
  2. 在组件卸载的时候用 dispose 代替 clear

clear 和 dispose

clear()

  1. 清空当前图表的配置项和数据,但不会销毁 ECharts 实例。
  2. 执行后,如果要重新绘制不需要重新 init
  3. 适合场景:需要保留实例,只是重新绘制内容。

dispose()

  1. 彻底销毁 ECharts 实例,释放 DOM 上的引用和事件监听器。
  2. 销毁后,如果要重新绘制,必须重新 echarts.init()。
  3. 适合场景:图表生命周期结束 or DOM 被移除。

错误代码

vue
<template>
  <div class="chart-container">
    <div ref="chartRef" class="chart"></div>
  </div>
</template>

<script setup>
import * as echarts from "echarts";
import { ref, onMounted, onUnmounted } from "vue";
import axios from "axios";

const chartRef = ref(null);
let chartInstance = null;
let timer = null;

async function fetchDataAndRender() {
  try {
    const res = await axios.get("/api/chartData");
    const data = res.data;

    // ❌ 每次都重新 init —— 会创建多个 ECharts 实例
    chartInstance = echarts.init(chartRef.value);

    const option = {
      title: { text: "实时数据" },
      xAxis: { type: "category", data: data.time },
      yAxis: { type: "value" },
      series: [{ data: data.value, type: "line" }],
    };

    // 设置新数据
    chartInstance.setOption(option);
  } catch (e) {
    console.error(e);
  }
}

onMounted(() => {
  // ❌ 每5秒拉取一次数据并重绘
  fetchDataAndRender();
  timer = setInterval(fetchDataAndRender, 5000);
});

onUnmounted(() => {
  // ❌ 销毁时只 clear 而非 dispose,旧实例仍在内存中
  if (chartInstance) {
    chartInstance.clear(); // 只清空内容,不释放事件与内存
  }
  clearInterval(timer);
});
</script>

<style scoped>
.chart {
  width: 100%;
  height: 400px;
}
</style>