D3 简介

Vue3 实战封装

理清 D3 和 Vue 的分工原则,把图表封装成可复用的 Vue3 组件,处理好数据更新与卸载清理。

🎯 引言

前面的例子都把 D3 代码直接写在业务组件里,画一个图就要复制一遍比例尺、坐标轴、事件绑定,重复又难维护。学会这篇文章,你能理清 D3 和 Vue 的分工原则,把图表封装成可复用的 Vue3 组件,并处理好数据更新和卸载清理,完成从“会画”到“会用”的最后一跃。


🧱 D3 和 Vue 的分工原则

D3 和 Vue 都想操作 DOM,如果让它们管同一批元素,就会打架。先看一个错误示范:

<template>
    <!-- Vue 用 v-for 渲染了柱子 -->
    <svg>
        <rect v-for="d in list" :key="d.day" :height="d.value" />
    </svg>
</template>

<script setup>
// 同时 D3 又来修改这批柱子
d3.selectAll('rect').attr('fill', 'red');
</script>

结果:Vue 下次更新时会按自己的虚拟 DOM 把 D3 改的内容全部覆盖掉,D3 删掉的元素 Vue 也不知道——两边互相干扰,画面错乱。

社区的共识是一句话:

Vue 只负责渲染空容器,容器内部的图形全部交给 D3。

正确姿势就三条:

  1. 模板里只写一个带 ref 的空 <svg>不写任何 v-for
  2. 容器内部的 rectpath 等图形,全由 D3 创建和更新。
  3. D3 的初始化代码放在 onMounted 里,这时容器才真实存在。

🛠 封装 BarChart 组件

按这个原则,把比例尺、坐标轴、数据绑定全部收进组件,对外只暴露一个 data 属性:

components/BarChart.vue
<template>
    <svg ref="svgRef" class="chart"></svg>
</template>

<script setup>
import { onMounted, ref, watch } from 'vue';
import * as d3 from 'd3';

const props = defineProps({
    data: {
        type: Array,
        required: true,
    },
});

const svgRef = ref(null);

const render = () => {
    const margin = { top: 20, right: 20, bottom: 30, left: 40 };
    const width = 480 - margin.left - margin.right;
    const height = 280 - margin.top - margin.bottom;

    // 先清空再重画,保证不会重复叠加
    const root = d3.select(svgRef.value);
    root.selectAll('*').remove();

    const svg = root
        .append('g')
        .attr('transform', `translate(${margin.left}, ${margin.top})`);

    const xScale = d3
        .scaleBand()
        .domain(props.data.map((d) => d.day))
        .range([0, width])
        .padding(0.2);

    const yScale = d3
        .scaleLinear()
        .domain([0, d3.max(props.data, (d) => d.value)])
        .range([height, 0])
        .nice();

    svg.selectAll('rect')
        .data(props.data)
        .join('rect')
        .attr('x', (d) => xScale(d.day))
        .attr('y', (d) => yScale(d.value))
        .attr('width', xScale.bandwidth())
        .attr('height', (d) => height - yScale(d.value))
        .attr('fill', '#5470c6');

    svg.append('g')
        .attr('transform', `translate(0, ${height})`)
        .call(d3.axisBottom(xScale));

    svg.append('g').call(d3.axisLeft(yScale));
};

onMounted(render); // 挂载后画第一次

// 数据变化时自动重画
watch(() => props.data, render, { deep: true });
</script>

<style scoped>
.chart {
    width: 480px;
    height: 280px;
}
</style>

整个组件就四个部分,对照着看就清楚了:

  1. defineProps:声明接收的 data
  2. render:完整的画图逻辑(前几篇学过的内容原样搬进来)。
  3. onMounted(render):挂载后画第一次。
  4. watch:数据变了自动重画,deep: true 保证数组内部修改也能监听到。

使用时一行搞定:

views/Sales.vue
<template>
    <BarChart :data="list" />
</template>

<script setup>
import { ref } from 'vue';
import BarChart from '@/components/BarChart.vue';

const list = ref([
    { day: '周一', value: 120 },
    { day: '周二', value: 200 },
    { day: '周三', value: 150 },
]);
</script>
render 里用了最朴素的“清空重画”策略:selectAll('*').remove() 删掉所有图形再重新渲染。简单可靠,绝大多数场景性能完全够用。追求丝滑动画时,可以换成动画篇学的“重新绑定数据 + transition”增量更新。

下面就是这个封装思路的最终效果——入场生长动画、刷新数据的更新过渡、悬停提示框,全部具备:

当前 5 条数据

💡 Vue 中使用 D3 的常见坑

原因对策
图形不显示onMounted 前初始化,容器不存在画图代码放进 onMounted
数据变了图没变没有监听 props 变化watch 监听并在回调里重画
图形越画越多、重叠重画前没清空旧图形渲染前先 selectAll('*').remove()
画面错乱Vue 和 D3 同时操作同一批节点模板只留空容器,内部全交给 D3
组件卸载后报错window 上的事件监听没移除onBeforeUnmount 里移除监听

D3 不像 ECharts 有 dispose 方法,组件卸载时 DOM 会被 Vue 一并移除,一般不需要额外清理。唯一要记住的是:如果你在 windowdocument 上绑了监听(如 resize),必须在 onBeforeUnmount 里移除。


🧾 小节总结

  • 分工原则:Vue 渲染空容器,D3 管理容器内部的图形,互不越界。
  • 同一批节点只能由一方管理,混用必乱。
  • 封装四部曲:defineProps 接数据、render 画图、onMounted 首绘、watch 重画。
  • 重画策略:简单场景“清空重画”,追求动画用“重新绑定 + transition”。
  • D3 无需 dispose,但 window 上的事件监听要在卸载时手动移除。

❓ 知识问答

Q1:父组件数据是异步获取的,初始是空数组怎么办?

render 开头加保护:if (!props.data.length) return;watch 会在数据到达后自动触发重画。

Q2:一个页面放多个 BarChart 会互相影响吗?

不会。每个组件实例有自己的 svgRef,D3 的选择都基于这个容器,天然隔离。

Q3:为什么不直接用 v-for 渲染柱子,非要 D3?

简单静态图表确实可以。但 D3 的比例尺、坐标轴、动画、缩放是成套体系,v-for 只能解决“画出来”,解决不了“专业地画”。

Q4:社区有现成的 D3 + Vue 封装库吗?

有一些,但 D3 的魅力就在于完全自定义,实际项目大多按本课的方式自己封装。掌握原理后,看任何封装库都能一眼看穿。


🧪 小练习

基于本课的 BarChart 组件完成一个“数据看板”:

  1. 页面上放一个 BarChart 和一个“刷新数据”按钮。
  2. 点击按钮生成 5 条随机数据并赋值给 list,观察图表自动重画。
  3. 进阶:把“清空重画”换成“重新绑定数据 + transition”,观察柱子平滑升降。
views/Dashboard.vue
<template>
    <!-- 请在这里编写代码 -->
</template>

<script setup>
// 请在这里编写代码
</script>

🎉 恭喜你已经完整掌握 D3 的核心技能啦!从数据绑定到比例尺、坐标轴、图形生成器、动画交互,再到 Vue3 封装,你已经具备用 D3 实现定制化可视化的能力。D3 的世界很大,力导向图、地图、树图都在向你招手,快去探索吧!