LogicFlow 事件机制完全指南:eventCenter 通讯中心、事件命名规范与源码级实现解析
【免费下载链接】LogicFlowA flow chart editing framework focus on business customization. 专注于业务自定义的流程图编辑框架,支持实现脑图、ER图、UML、工作流等各种图编辑场景。项目地址: https://gitcode.com/GitHub_Trending/lo/LogicFlow
eventCenter是 LogicFlow 内部的通讯中心,负责以低耦合方式连接图编辑器的各个模块:节点、边、锚点、画布、历史记录与选区等通过它发布或监听事件,业务侧则通过lf.on()/lf.off()等实例方法订阅这些事件。本文以 packages/core/src/event/event.md 为骨架,结合 eventEmitter.ts、eventArgs.ts 与各视图层源码,系统讲解事件命名规范、事件对象结构、发布/订阅 API 及其底层实现,帮助你在基于 LogicFlow 的业务开发中正确使用事件系统,并能在需要时写出符合框架规范的自定义事件。
一、eventCenter 是什么:低耦合的模块间通讯中枢
eventCenter是 LogicFlow 内部的"事件总线"(Event Bus)。在流程图的复杂交互场景中,节点点击、边删除、画布平移等行为往往涉及多个模块联动,如果模块之间直接互相引用,会形成强耦合、难以维护。LogicFlow 的解法是:
- 所有内部模块统一通过
eventCenter发布(emit)事件; - 关心某个事件的模块(或业务用户)通过
eventCenter监听(on)事件; - 发布方与监听方互不感知对方的存在,只依赖事件名这一"约定"完成通信。
从源码结构看,GraphModel.ts 在构造函数中创建了eventCenter = new EventEmitter()实例,并将其挂载为graphModel.eventCenter;LogicFlow.tsx 在初始化时取出this.graphModel.eventCenter,用于驱动历史记录(History)、吸附线(snapline)、键盘(Keyboard)等内置能力。也就是说,整个画布从创建那一刻起,所有模块共享同一个事件中心实例。
在开发过程中,使用eventCenter也需要遵循一些规范,下文逐一展开。
二、事件命名规范:namespace:eventName
为什么需要 namespace
LogicFlow 规定事件命名遵循namespace:eventName的结构,同类eventName通过 namespace 来区分。例如node和edge都会抛出click事件,但观察者可能是不同群体——"节点被点击"与"边被点击"的业务含义完全不同。通过 namespace 前缀,事件监听变得更精确,不容易串扰、不容易出错。
以node:click和edge:click为例:
node:click仅在节点被点击时触发;edge:click仅在边被点击时触发。
如果业务只关心节点点击,只需lf.on('node:click', ...),完全不会收到边的点击通知。
core 包中已定义的 namespace 全集
原文档列出了 core 包中定义的核心 namespace:node(节点事件)、edge(边事件)、anchor(锚点事件)、blank(画布空白区域事件)、history(历史记录事件)、selection(选区事件)。
从 constant/index.ts 的EventType枚举可以看到,实际事件体系比这更完整,除了上述 6 类外还包括:
| namespace | 含义 | 代表事件(EventType 常量) |
|---|---|---|
node | 节点事件 | node:click、node:dbclick、node:drag、node:resize、node:rotate、node:properties-change等 |
edge | 边事件 | edge:click、edge:dbclick、edge:adjust、edge:exchange-node等 |
anchor | 锚点事件 | anchor:click、anchor:dragstart、anchor:drop、anchor:dragend等 |
blank | 画布空白区域事件 | blank:mousedown、blank:click、blank:contextmenu等 |
history | 历史记录事件 | history:change |
selection | 选区事件 | selection:mousedown、selection:drag、selection:contextmenu等 |
text | 文本事件 | text:click、text:update、text:clear等 |
label | label 插件文本事件 | label:click、label:should-add、label:batch-add等 |
element | 元素公共事件 | element:click(node:click与edge:click的并集) |
graph | 画布事件 | graph:transform、graph:rendered、graph:updated、graph:resize |
connection | 连线校验事件 | connection:not-allowed |
adjustPoint | 折线调整点事件 | adjustPoint:mousedown、adjustPoint:drag等 |
提示:事件字符串常量集中在
EventType枚举中,开发插件或深入源码时优先引用枚举而不是手写字符串,可避免拼写错误。
三、事件发布(emit)的两种途径与组件级规范
原文档明确了内部抛出事件的两种方式,这在阅读源码时可以得到完整印证:
途径一:graphModel 中通过this.eventCenter抛出
graphModel自身持有eventCenter实例,需要广播模型状态变化时直接调用其emit方法。例如节点删除(GraphModel.ts):
this.eventCenter.emit(EventType.NODE_DELETE, { data: nodeData })边的新增、删除同理(GraphModel.ts、GraphModel.ts);节点properties变化则由 BaseNodeModel.ts 通过this.graphModel.eventCenter.emit(EventType.NODE_PROPERTIES_CHANGE, ...)抛出。
途径二:组件通过 props 获取 eventCenter 后抛出
视图组件有两类获取方式,原文档明确指出:
- 部分组件直接从
props中获取eventCenter实例。例如折线边组件 PolylineEdge.tsx 中直接eventCenter.emit(EventType.EDGE_ADJUST, { data: polylineModel.getData() })。 - 另一部分组件从
props中获取graphModel,再通过graphModel.eventCenter拿到实例。例如锚点组件 Anchor.tsx 与节点组件 BaseNode.tsx:
// BaseNode.tsx 中抛出节点单击 / 双击事件 graphModel.eventCenter.emit(EventType.NODE_DBCLICK, eventOptions) graphModel.eventCenter.emit(EventType.NODE_CLICK, eventOptions)画布空白区域的blank:*事件则在 CanvasOverlay.tsx 中抛出,例如blank:click、blank:contextmenu、blank:mousedown。
组件生命周期规范:销毁时必须取消监听
原文档特别强调:如果组件内部监听了eventCenter事件,在组件销毁的时候,必须取消这些监听。这背后是内存泄漏问题——事件总线持有回调引用,若组件卸载后监听未移除,回调仍会被触发并持有组件上下文。规范做法是:
componentDidMount() { this.eventCenter.on('node:click', this.handleNodeClick) } componentWillUnmount() { this.eventCenter.off('node:click', this.handleNodeClick) }四、事件对象:emit 的第二参数即回调入参
在使用emit方法抛出事件时,可以传递一个对象作为第二个参数,该对象将作为对应事件监听器回调函数的入参。事件对象可以包含任何与当前事件相关的信息,比如节点的 id、边的 id、原生鼠标事件对象等。
原文档以node:click事件为例给出了完整的发布与监听代码:
// 抛出 node:click 事件 eventCenter.emit('node:click', { data: { // 节点数据 }, e: MouseEvent, position: { // 鼠标点击的位置信息 } }); // 监听 node:click 事件 eventCenter.on('node:click', (event) => { console.log(event); // event 即为抛出事件时传递的对象 }); // 使用解构赋值可以便捷地获取事件对象中的信息 eventCenter.on('node:click', ({ data, e, position }) => { console.log(data, e, position); });事件对象的 TypeScript 类型定义
从源码结构看,core 包在 eventArgs.ts 中为所有内置事件定义了严格的参数类型(聚合导出为EventArgs)。以节点事件为例,其参数通过NodeEventArgsPick按事件场景"按需选取":
node:click/node:dbclick/node:contextmenu:{ data, e, position },其中position是鼠标触发点相对画布左上角的坐标(ClientPosition);node:mousedown/node:mouseup/node:mouseenter/node:mouseleave:{ data, e };node:drag/node:mousemove:额外包含deltaX、deltaY(鼠标在 X/Y 轴移动的距离);node:resize:额外包含preData(上一个状态的节点数据)、model以及index(Resize 时调整的是哪个控制点);node:click还额外混入了ClickEventArgs的isSelected(点击后节点是否处于选中状态)与isMultiple(是否为多选状态)。
其它事件族同理:
- 边事件(
EdgeEventArgs):edge:click等为{ data, e, position };edge:exchange-node的参数为{ data: { newEdge, oldEdge } }; - 锚点事件(
AnchorEventArgs):参数包含data(锚点配置)、e、nodeModel(锚点所属节点),anchor:drop与anchor:dragend在成功连线时还会携带edgeModel; - 画布事件(
BlankEventArgs):参数为{ e, position }; - 选区事件(
SelectionEventArgs):selection:contextmenu参数为{ data, e, position }; - 公共事件(
CommonEventArgs):element:click为{ data, e, position };graph:transform为{ type, transform };graph:rendered为{ data, graphModel }; - 历史事件(
HistoryEventArgs):history:change的参数为{ data: { undos, redos, undoAble, redoAble } }。
值得说明的是:
CallbackArgs<T>的机制是"如果事件名不是内部定义的事件类型,那么允许用户抛出任何类型的参数,类型由用户自己保证"(见 eventEmitter.ts)。这意味着你可以自由定义自己的自定义事件,并在回调中接收任意结构的数据。
五、发布/订阅 API 详解与底层实现
eventCenter的核心实现是 eventEmitter.ts 中的EventEmitter类(同时导出了EventArgs类型)。它提供四个核心方法与若干辅助能力,并有对应单元测试验证(见 event/event.test.ts)。
5.1 on:注册监听
on(evt: string, callback: EventCallback, once?: boolean): voidevt支持用逗号分隔的多个事件名,内部会逐个拆分并 trim 后注册(evt?.split(',').forEach(...));- 同一事件可注册多个回调,按注册顺序依次触发;
once参数为true时,回调触发一次后自动移除(once()方法即this.on(evKey, callback, true)的语法糖)。
5.2 emit:触发事件
emit(evts: string, eventArgs?: EventCallback): voidemit同样支持逗号分隔的多个事件名。它的执行流程包含一个值得注意的细节:每个事件触发时,除了执行该事件自身的回调列表,还会执行通配符事件'*'下的所有回调:
const events = this._events[evt] || [] const wildcardEvents = this._events[WILDCARD] || [] // WILDCARD = '*' doEmit(events) doEmit(wildcardEvents)这意味着你可以通过eventCenter.on('*', callback)监听画布上的所有事件,这对调试、埋点统计等场景非常有用。同时,once监听在触发时会被就地移除(es.splice(i, 1)),并同步修正遍历长度。
5.3 off:取消监听
off的语义分三种情况(源码注释明确说明):
evts为空:清除所有事件的监听器(this._events = {});evts非空、callback为空:清除指定事件的所有监听器(delete this._events[evt]);evts非空、callback非空:通过对象引用比较(events[i].callback === callback)清除指定事件的指定监听器,事件监听器被清空后该事件键也会被删除。
// 用法示例 eventCenter.off('node:click') // 移除 node:click 的所有监听 eventCenter.off('node:click', handler) // 仅移除 handler 这个监听 eventCenter.off('') // 移除全部监听5.4 once:只监听一次
once(evt: string, callback: EventCallback): void注册后首次触发即自动移除,等价于on(evt, callback, true)。注意once注册的监听在触发一次后,若没有其它监听残留,对应事件键会从_events中删除(测试用例 event.test.ts 验证了这一行为)。
5.5 辅助方法:getEvents 与 destroy
getEvents():返回当前所有事件及其回调列表(this._events),可用于调试;destroy():清空全部事件监听(this._events = {}),GraphModel.ts 在销毁流程中调用它完成事件系统的资源释放。
六、业务侧如何使用:lf 实例的 on/off/once/emit
普通用户并不需要直接接触eventCenter实例。LogicFlow 实例lf已经将graphModel.eventCenter的四个核心方法透传为实例方法(LogicFlow.tsx):
lf.on('node:click', ({ data, e, position }) => { ... }) lf.off('node:click', handler) lf.once('graph:rendered', ({ data, graphModel }) => { ... }) lf.emit('custom:event', { foo: 'bar' })也就是说,lf.on(evt, callback)内部等价于this.graphModel.eventCenter.on(evt, callback)。用户可以通过lf监听框架抛出的所有内置事件,也可以通过lf.emit向eventCenter广播自定义事件,实现与插件、与其他模块之间的解耦通信。
典型实践示例:监听节点点击与画布渲染
// 节点点击,事件对象解构出 data / e / position lf.on('node:click', ({ data, e, position }) => { console.log('点击了节点', data.id, '坐标', position.x, position.y) }) // 画布渲染完成(lf.render() 被调用后触发) lf.once('graph:rendered', ({ data, graphModel }) => { console.log('首次渲染完成,共', graphModel.nodes.length, '个节点') }) // 画布尺寸变化(已做 16ms 防抖) lf.on('graph:resize', ({ contentRect }) => { console.log('画布新尺寸', contentRect.width, contentRect.height) })关于graph:updated的注意事项
eventArgs.ts 中的注释对graph:updated给出了明确建议:该事件在lf.render(graphData)被调用后、或改变画布(graphModel)上的属性后都会触发。如果你只是想在某次主动修改后执行一次操作,建议注册事件后在回调中及时注销该事件,或者使用once代替on,因为其它属性变更也可能触发该事件,导致回调被意外多次执行。
七、源码佐证:一条事件从抛出到监听的完整链路
以node:click为例,梳理完整的事件流(以源码为准):
- 抛出端:用户在画布上点击节点,节点视图组件 BaseNode.tsx 捕获原生点击事件后调用
graphModel.eventCenter.emit(EventType.NODE_CLICK, eventOptions),其中eventOptions即{ data, e, position }结构; - 总线:
EventEmitter.emit根据事件名node:click找到回调列表,逐个调用回调(并将once监听移除),随后执行通配符'*'监听; - 监听端:业务侧
lf.on('node:click', callback)注册的回调被调用,入参即eventOptions。
整条链路中,抛出端不关心谁在监听,监听端不关心事件从哪来,双方只依赖node:click这个命名约定——这正是eventCenter低耦合设计的价值所在。
八、总结与开发规范速查
| 关注点 | 规范 |
|---|---|
| 事件命名 | 一律使用namespace:eventName结构,core 包事件常量见 constant/index.ts 的EventType |
| 事件参数 | emit的第二个参数即回调入参,内置事件参数类型见 eventArgs.ts 的EventArgs |
| 发布方式 | 模块/组件内统一通过eventCenter(或graphModel.eventCenter)调用emit |
| 订阅方式 | 业务侧使用lf.on/lf.off/lf.once,与eventCenter方法一一对应 |
| 组件生命周期 | 组件销毁时必须off掉自己注册的监听,避免内存泄漏 |
| 单次监听 | 用once,或监听到目标后立即off(尤其注意graph:updated的触发条件) |
| 全局调试 | eventCenter.on('*', ...)可监听所有事件;getEvents()可查看当前全部监听 |
掌握这套事件机制,你就能精准订阅节点、边、锚点、画布、历史、选区等各类交互,也能基于lf.emit构建属于自己的模块间通信协议,在业务自定义的道路上进一步发挥 LogicFlow 框架的扩展能力。
【免费下载链接】LogicFlowA flow chart editing framework focus on business customization. 专注于业务自定义的流程图编辑框架,支持实现脑图、ER图、UML、工作流等各种图编辑场景。项目地址: https://gitcode.com/GitHub_Trending/lo/LogicFlow
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考