本文将通过代码精简、核心流程图解和深度原理解析三个维度,带你彻底搞懂 React 与 G6 的协同工作机制。
我们剥离掉样式细节(颜色、阴影、具体的坐标计算),只保留数据流和控制流的核心骨架。
第一部分:核心逻辑精简代码 (带详细注释)
这部分代码展示了 React 如何作为“大脑”指挥 G6 这个“绘图员”。
1. EAMapUtil.js (绘图员与交互规则)
JavaScript
import G6 from "@antv/g6";
// 1. 注册自定义节点:告诉 G6 节点长什么样
export const registerEAMapNode = () => {
G6.registerNode("EA-node", {
// 核心绘制方法:当 G6 需要渲染节点时调用
draw(cfg, group) {
// A. 绘制“身体”:主矩形卡片
const keyShape = group.addShape("rect", {
attrs: { width: 200, height: 80, fill: "#fff", stroke: "#ccc" },
name: "card-container" // 给图形命名,方便后续查找
});
// B. 绘制文字:显示标题
group.addShape("text", {
attrs: { text: cfg.EAName, x: 10, y: 20, fill: "#000" },
});
// C. 绘制“加号/展开”按钮组 (默认隐藏,根据状态显示)
// 注意:这里用 addGroup 创建子容器,方便统一控制显隐
const expandBtnGroup = group.addGroup({ name: "expand-btn-group" });
expandBtnGroup.addShape("circle", { attrs: { r: 10, fill: "blue" } });
expandBtnGroup.addShape("text", { attrs: { text: "+", fill: "#fff" } });
// 初始渲染时,根据数据中的 collapsed 字段决定是否隐藏
// 如果没有子节点,或者已经展开,就隐藏这个加号
if (!cfg.children || cfg.children.length === 0 || !cfg.collapsed) {
expandBtnGroup.hide();
}
return keyShape; // 必须返回主图形,用于计算包围盒
},
// 核心状态响应:当 React 调用 graph.setItemState 时触发
setState(name, value, node) {
const group = node.getContainer(); // 获取该节点的所有图形容器
// 处理“展开/收起”状态变化
if (name === "Expanded") {
const expandBtn = group.find((item) => item.get("name") === "expand-btn-group");
if (value) {
// 状态为 true (已展开),隐藏加号
expandBtn.hide();
} else {
// 状态为 false (已收起),显示加号
expandBtn.show();
}
}
}
});
};
// 2. 注册交互行为:告诉 G6 点击后通知谁
// 这里接收一个来自 React 的回调函数 handleExpandClick
export const registerEAMapBehavior = (handleExpandClick) => {
G6.registerBehavior("EA-interaction", {
getEvents() {
return { "node:click": "onClick" }; // 监听节点的点击事件
},
onClick(evt) {
const { item, target } = evt; // item是节点实例,target是具体点击的图形(比如加号圆圈)
const parentGroup = target.get("parent"); // 获取被点击图形的父组
// 判断用户是否点击了“加号”按钮组
if (parentGroup && parentGroup.get("name") === "expand-btn-group") {
// !!! 核心:阻断 G6 内部逻辑,将控制权交给 React 传入的回调函数
handleExpandClick(item);
}
}
});
};
2. index.js (React 控制器)
JavaScript
import React, { useRef, useEffect } from "react";
import G6 from "@antv/g6";
import { registerEAMapNode, registerEAMapBehavior } from "./EAMapUtil";
export default function EAMap() {
const containerRef = useRef(null); // DOM 容器
const graphRef = useRef(null); // G6 实例引用
// 核心逻辑:异步加载子节点
// 这个函数会被注入到 G6 的 Behavior 中
const addUnitAsync = async (nodeItem) => {
const model = nodeItem.getModel(); // 获取当前节点的数据
const graph = graphRef.current;
// 1. 如果数据还没加载过 (防重复请求)
if (!model.isSearched) {
// 模拟 API 请求
const newChildren = await fetchChildrenData(model.id);
// 2. 直接修改 G6 节点的数据模型 (Mutable Operation)
model.children = newChildren;
model.isSearched = true; // 标记已加载
// 3. 告诉 G6:数据变了,请刷新子树
// updateChildren 会处理数据挂载并自动重绘连接线
graph.updateChildren(newChildren, model.id);
} else {
// 如果已有数据,单纯切换展开状态
graph.updateItem(nodeItem, { collapsed: false });
}
// 4. 更新视觉状态:触发 EAMapUtil 中的 setState
// 让加号消失,或者做其他样式变更
graph.setItemState(nodeItem, "Expanded", true);
// 5. 自动排版:重新计算节点位置
graph.layout();
};
useEffect(() => {
if (!graphRef.current) {
// A. 注册自定义元素和行为
// 注意:把 React 组件内的函数 addUnitAsync 传进去,闭包打通了 React 和 G6
registerEAMapNode();
registerEAMapBehavior(addUnitAsync);
// B. 初始化图实例
const graph = new G6.TreeGraph({
container: containerRef.current,
width: 800,
height: 600,
modes: {
default: ["drag-canvas", "zoom-canvas", "EA-interaction"], // 启用我们注册的行为
},
layout: { type: "compactBox" }, // 树图布局
});
// C. 初始渲染
const initData = { id: "root", EAName: "根流程", children: [] };
graph.data(initData);
graph.render();
graphRef.current = graph;
}
}, []);
return <div ref={containerRef} style={{ height: "100vh" }} />;
}
第二部分:核心流程图 (Mermaid)
这张图展示了从用户点击到视图更新的完整闭环。
代码段
第三部分:完整解释说明
1. 初始化机制:依赖注入的桥梁
整个系统的关键在于 index.js 的 useEffect 中。
-
G6 独立王国:G6 作为一个 Canvas 库,它不认识 React 的 State 或 Context。
-
依赖注入:通过
registerEAMapBehavior(addUnit),我们将 React 内部定义的函数(拥有访问 API 能力、修改 React 状态能力的函数)传递给了 G6 的配置层。 -
结果:当 G6 内部捕获到点击事件时,它实际上是在执行 React 组件上下文中的代码。
2. 渲染机制:Canvas 图形组装 (draw)
在 EAMapUtil.js 中,节点渲染不是写 HTML,而是像搭积木一样:
-
Group(组):每个节点是一个
Group,相当于 HTML 的div。 -
Shape(图形):在组里画
rect(卡片背景)、text(文字)、circle(按钮背景)。 -
命名查找:给图形设置
name属性至关重要(如operateGroupWhenExpanded)。因为 Canvas 无法像 DOM 那样用querySelector,G6 依赖group.find(item => item.get('name') === 'xxx')来找到特定的图形进行操作(比如改变颜色、隐藏)。
3. 状态管理:命令式 vs 声明式
-
React (声明式):我们习惯
setState({ visible: true })然后 React 自动重绘。 -
G6 (命令式):在 Canvas 里,改变状态需要手动操作。
-
步骤 1:React 调用
graph.setItemState(node, 'Expanded', true)。 -
步骤 2:G6 触发
EAMapUtil.js里的setState方法。 -
步骤 3:在 setState 里,我们需要手动写逻辑:“找到那个加号圆圈,调用 .hide() 方法”。
这更像原本的 jQuery 或原生 DOM 操作逻辑。
-
4. 数据流转:Mutable Model(可变模型)
React 开发者通常习惯 Immutable(不可变)数据,但在 G6 中,操作通常是 Mutable(可变)的。
-
流程:
addUnit函数中,我们直接获取了节点的数据模型EAItem.getModel(),然后直接执行了currentEAItem.children.push(...)。 -
同步:修改了 JS 对象后,G6 并不知道数据变了。必须调用
graph.updateChildren或graph.changeData,G6 才会根据新的数据模型重新计算布局并重绘。
5. 交互控制:事件代理
代码中通过 registerBehavior 实现了事件代理。
-
我们不给每个“加号”绑定 click 事件(那样太重了)。
-
我们监听整个画布或节点的
node:click。 -
通过
target.get("name")或target.get("parent").get("name")来判断用户到底点在了卡片的哪个位置(是点在空白处选中,还是点在加号上展开)。这就是所谓的“命中检测”。
总结
这段代码的核心是利用 G6 强大的图渲染能力,结合 React 的业务逻辑处理能力。
-
EAMapUtil.js 是皮肤(定义长相)和神经末梢(感知点击)。
-
index.js 是大脑(决定数据怎么加载、何时更新)。
两者通过配置回调连接,数据变更通过直接修改 Model + 显式通知 Graph 更新来完成。