资讯动态

ET8.1事件监听机制详解:从底层原理到实战避坑

发布时间:2026/9/9 9:09:43 来源:尧图企业网站定制
1. 从ET框架聊起为什么事件机制如此重要做游戏服务端开发的兄弟们对ET框架应该不陌生。ETEntity Technology是一个基于.NET的分布式游戏服务端框架主打双端共享代码、组件化开发、协程驱动这几年在中小型团队和个人独立开发者里用得非常广。我是在ET 6.0时代开始接触的一路从6.0用到8.0再到现在的8.1中间踩过不少坑也见证了这套框架在事件机制上的几次重要迭代。今天要聊的就是ET8.1里的事件监听这玩意儿说起来只是框架的一小块但它贯穿了业务开发的方方面面掌握好了能极大提升开发效率掌握不好就会陷入事件怎么不触发回调怎么丢了的泥潭里。先给没接触过ET的朋友打个底。ET框架的核心思想是实体Entity加组件Component的组合模式所有游戏逻辑都挂在实体上通过组件来划分功能边界。而事件机制则是实体和组件之间、模块和模块之间通信的桥梁。打个比方如果ET框架是一个城市实体是建筑物组件是房间里的各种设施那事件系统就是贯穿整个城市的路网任何一处发生的事情要通过路网传达到需要响应的地方。没有路网每个建筑就是孤岛开发就会变成噩梦。ET8.1的事件监听体系本质上是做了一件很朴素的事把事情发生和事情处理彻底解耦。比如玩家死亡这件事可能同时需要触发播放动画、扣减货币、掉落物品、推送消息给客户端、更新排行榜等一堆逻辑。如果不用事件机制你得在死亡逻辑里一行一行地把所有后续处理都写一遍每加一个需求就要改动死亡逻辑本身代码很快就会腐化成一大坨。用事件机制死亡逻辑只需要发布一条UnitDead事件所有关心这个事件的模块各自注册监听自己处理自己的事谁来了谁走了互不干扰。这套思路现在看起来不算稀奇很多框架都有类似设计。但ET8.1事件监听的特别之处在于它和框架的协程体系、组件生命周期深度绑定整套机制在异步场景下的表现和一整套底层API是打通了的。这篇博客我会从事件系统的底层组成讲起拆解8.1版本里事件监听的完整链路——事件定义、处理器的编写、事件的发布、异常处理再到性能调优和常见坑点。不管你是刚开始接触ET的新手还是已经写了一阵子但总觉得事件这块有点玄学的老手这篇内容应该都能给你一些参考。我尽量用实际项目里会遇到的场景来讲所有代码示例都是基于ET8.1的源码风格手写的手头有工程的话可以直接对照着跑。2. 事件监听的底层设计与思路拆解2.1 事件系统是由哪几块组成的要想把ET8.1的事件监听用明白先得搞清楚这套系统里到底有哪些角色。ET的事件系统大致可以拆成四个部分事件类型标记、事件处理器、事件发布器、以及最底层的事件系统管理器EventSystem。事件类型标记在ET里就是EventType定义的一堆字符串常量。你可能会有疑问为什么用字符串而不是像C#里的event或者委托那样直接用类型这个设计其实有历史背景。ET的老版本6.x之前事件类型就是简单字符串后来虽然引入了泛型版本的事件参数但字符串事件名作为标识符的习惯保留了下来原因很实在字符串可以做静态配置可以序列化到配表也可以跨进程传递灵活性比强类型标识好很多。事件处理器就是那些继承了AEventT并标记了[Event(EventType.XXX)]特性的类。框架启动的时候EventSystem会扫描所有程序集把标记了Event特性的类自动注册到内部的事件字典里。这一步是ET事件系统的核心魔术它靠的是反射加特性的组合拳避免了手动注册的繁琐和遗漏。事件发布器不需要你自己实现EventSystem里的Publish就是干这个的。调用Publish的时候框架会根据事件类型去字典里找到所有注册的处理器逐个调用。最后是EventSystem管理器本身。它在8.1里是一个组件挂在Game.Scene下面所有事件的注册、查找、分发都经过它。这也意味着你在使用时需要先获取到这个组件的实例再调用它的Publish方法。清晰了这几个角色的职责再去看源码就不会乱。下面这张表帮你快速建立映射关系角色对应类型/接口职责事件类型标识EventType类中的字符串常量定义发生了什么事件参数自定义结构体/类如UnitDeadEvent封装事件上下文数据事件处理器继承AEventT并标记[Event]的类处理具体业务逻辑事件发布器EventSystem.Publish向所有处理器广播事件事件管理器EventSystem组件注册、查找、分发、异常捕获2.2 为什么非要用AEvent加Attribute这套组合这是我被问得最多的问题之一。很多从Unity开发转过来的同事习惯了Unity的UnityEvent或者C#的event委托看到ET这套要写单独的类、还要继承抽象类觉得麻烦不理解这么设计的意义在哪里。先说结论这套设计最大的优势是做到了处理逻辑的完全自描述。一个类就是一个事件处理器类名本身就表达了它处理什么事件标记的Event特性写明了它监听哪个事件类型基类AEventT的泛型参数T限定了事件参数的类型。三者结合起来所有信息都是显式的代码即文档。你拿到一个处理器类不需要去翻别的地方就知道这个类干什么的。相比之下C#原生的事件委托机制在小型项目里很顺手但一旦模块多了事件的订阅和反订阅散落在各处很容易出现生命周期问题。最常见的就是对象已经被销毁了但它的事件方法还在委托链上导致调用的时候报空引用。ET这套机制把事件处理器的生命周期交给框架管理你不需要手动订阅和取消订阅只要你创建了处理器类通常由框架在程序集扫描时自动创建框架就负责调用如果你的组件被销毁了框架在分发事件时会自动跳过。这从机制上规避了一大类生命周期Bug。还有一点值得说道AEventT这个抽象类在8.1里定义的是异步接口Run也就是说事件处理方法天然支持await。这太重要了。游戏逻辑里大量操作是异步的比如等待服务器返回、加载资源、延时执行如果用同步事件委托遇到异步逻辑就得硬着头皮在事件处理里开协程或者另想办法。ET8.1直接把异步融入到事件处理器的基类设计里你写的处理器天然就是异步方法怎么写都顺。2.3 异步与同步ET8.1事件系统的取舍既然说到异步这块单独拿出来聊一下。ET8.1的事件接口设计成异步ETTask的但发布事件时可以选同步等待还是异步不等待。这个灵活性是8.x版本在事件系统中比较大的改进。在老版本里Publish事件要么完全同步要么交给线程池处理缺少中间选项。同步派发在事件处理逻辑比较重的时候会卡住调用线程尤其是在逻辑主线程里执行时影响明显但如果全部异步派发又可能带来竞态问题——某个事件的后续处理不保证在同一个逻辑帧内完成如果业务上依赖严格的先后顺序就很容易出问题。ET8.1的EventSystem里提供了两种派发方式PublishAsync是异步等待型发布事件后await所有处理器执行完成Publish则是同步派发内部会把所有处理器直接在当前线程上执行完但因为处理器本身的Run方法是异步的所以每个处理器内部如果需要异步操作还是能通过协程来挂起。这种设计兼顾了业务上的有序性要求处理器调用的顺序是有保证的和单个处理器内部的异步自由度处理逻辑可以随意await。在实际项目里我个人习惯是对时序敏感的事件用同步Publish对不敏感的事件用异步PublishAsync。比如玩家死亡和扣减货币这两个之间是有强依赖的都得用同步但死亡和推送世界聊天广播这种就完全可以用异步谁先谁后无所谓。3. 核心代码实操从定义到派发的完整链路3.1 第一步定义事件类型与事件参数事件类型的定义在ET里通常集中放在一个静态类里。项目里统一的写法是这样public static class EventType { // 玩家死亡 public const string UnitDead UnitDead; // 玩家升级 public const string UnitLevelUp UnitLevelUp; // 物品使用 public const string ItemUsed ItemUsed; // 任务进度变化 public const string TaskProgressChanged TaskProgressChanged; }字符串常量的好处刚才说了可以做配置映射还能在客户端和服务端之间共用。但需要注意命名规范我个人建议事件名用模块名动作的组合比如UnitDead、ItemUsed这种避免事件多了之后重名或混淆。ET框架本身没有强制命名规则但团队协作时一定要定好规范否则后期找事件定义会是灾难。事件参数类型ET8.1强烈推荐使用结构体而不是类。原因很简单结构体在栈上分配避免堆内存分配和GC压力。游戏服务端内存要求很高每帧都可能发布大量事件如果都用classGC会很难看。看一下ET源码里的标准写法public struct UnitDeadEvent { public Unit Unit { get; set; } public Unit Attacker { get; set; } public long Damage { get; set; } }参数结构体里放的是事件发生时刻的上下文快照。不用放太多字段够用就行事件参数越轻量复制成本和内存压力越低。3.2 第二步编写事件处理类事件处理类是事件监听的核心写起来很简单[Event(EventType.UnitDead)] public class UnitDeadHandler : AEventUnitDeadEvent { protected override async ETTask Run(UnitDeadEvent args) { // 从参数里取数据 Unit unit args.Unit; Unit attacker args.Attacker; long damage args.Damage; // 处理掉落逻辑 await Game.Scene.GetComponentDropComponent().SpawnDropsAsync(unit, damage); // 处理任务进度 await Game.Scene.GetComponentTaskComponent().NotifyKillTaskAsync(unit, attacker); // 推送消息给客户端 Game.Scene.GetComponentMessageSender().SendUnitDead(unit.Id, attacker.Id, damage); } }注意几个关键点。类名我建议用事件名Handler的格式比如UnitDeadHandler这样在IDE里看文件列表就能快速定位。类上必须标记[Event(EventType.UnitDead)]特性框架靠这个特性去识别这个处理类要监听哪条事件。继承AEventUnitDeadEvent后必须重写Run方法事件参数类型由泛型参数决定。框架在扫描程序集的时候会实例化所有标记了Event特性的事件处理类所以Handler本身是由框架管理的单例不需要你自己new。这里有一个容易踩的坑千万不要在Handler里保存状态比如某个成员变量标记处理了几次因为框架复用了同一个实例在多线程或多次调用之间状态是共享的保存状态会引发数据竞争和逻辑错误。Handler应该是无状态的所有数据都从事件参数里取。3.3 第三步发布事件发布事件的地方就是业务逻辑里事情发生的时刻。比如死亡逻辑里public class UnitCombatComponent : Entity { public async ETTask KillUnit(Unit target, Unit attacker) { // ... 扣血、结算等逻辑 // 发布玩家死亡事件 await Game.Scene.GetComponentEventSystem().PublishAsync(new UnitDeadEvent { Unit target, Attacker attacker, Damage lastDamage }); } }发布方法在EventSystem组件上。8.1版本推荐直接通过Game.Scene获取EventSystem组件因为EventSystem在框架启动时已经注册到Game.Scene上了。PublishAsync是异步派发会等待所有处理器执行完成如果不想等待可以调用Publish方法它内部会同步执行处理器但不等异步逻辑。有一点必须注意发布事件时传的参数结构体在异步派发过程中会被多个处理器读取。如果处理器内部await了很长时间而这个结构体里的引用对象被外部修改了就会读到脏数据。所以我在实际项目中要求所有事件参数里的引用型字段比如Unit对象在事件发布后不允许被修改要用的话先在发布前打好快照。3.4 一个完整的事件链路跑起来需要哪些配置上面三步是事件监听的最小闭环但要在ET8.1里真正跑起来还需要确保框架完成了扫描注册。ET框架在初始化时CodeTypes类的Init方法里会扫描所有程序集并收集标记了Event特性的类型所以只要你的Handler类在服务端主程序集或依赖的程序集里就会被自动发现。这里提一下程序集的坑。如果你在某个DLL里写了个事件处理器但这个DLL没有被主程序引用或放到指定目录框架就扫不到它事件就一直不触发。排查思路很简单先看Handler类所在的程序集是否被主程序引用了。在ET8.1的多程序集架构里服务端逻辑程序集通常是Main如果你的代码在别的DLL里需要在CodeTypes的扫描列表里加上那个DLL或者直接将代码放到框架默认扫描的程序集下。事件注册的最后一步在ET里已经自动化了但你仍然可以在自定义初始化逻辑里通过EventSystem的RegisterEvent方法手动注册事件这在做热更新或者动态加载插件的时候会用到// 手动注册事件参数是事件类型字符串和处理器类型 Game.Scene.GetComponentEventSystem().RegisterEvent(EventType.UnitDead, typeof(UnitDeadHandler));不过8.1正常情况下不用手动注册只有在搞动态加载、插件系统这种东西的时候才会用到。4. 事件监听的高级玩法与实际项目中的应用场景4.1 多个处理器监听同一事件的顺序问题事件机制的一大常见疑问是如果五个处理器都监听了UnitDead它们之间的执行顺序是怎样的ET8.1的EventSystem内部维护了一个按类型分类的处理器字典同一个事件类型下的处理器存储顺序受程序集扫描顺序影响不是稳定的。也就是说你不应该依赖多个处理器之间的执行顺序来传递状态计算结果。但在实际项目里有时确实需要顺序。比如玩家升级事件一个处理器要更新玩家战力另一个处理器要推送升级飘字到客户端。战力更新必须在飘字之前完成否则客户端拉新战力时会拉旧数据。解决这个问题有两种思路。第一种是把有先后依赖的逻辑合并到一个处理器里在Handler内部用代码顺序控制[Event(EventType.UnitLevelUp)] public class UnitLevelUpHandler : AEventUnitLevelUpEvent { protected override async ETTask Run(UnitLevelUpEvent args) { Unit unit args.Unit; // 先更新战力 await Game.Scene.GetComponentClipComputeComponent().RefreshAsync(unit); // 再推送飘字 Game.Scene.GetComponentMessageSender().SendLevelUp(unit.Id, args.NewLevel); // 最后解锁新的技能 await Game.Scene.GetComponentSkillComponent().UnlockSkillsAsync(unit, args.NewLevel); } }第二种是用事件内部的子事件流程。先派发一次LevelUpStarted事件做预处理等所有处理器执行完再派发LevelUpCompleted事件做后续工作。这样子事件天然有序且每个处理器的职责依然单一。在实际项目里我比较推荐第二种因为逻辑一旦复杂把什么都塞进一个Handler会非常臃肿而且很难复用。事件拆成多个阶段可以让不同模块在不同阶段精准插桩。4.2 事件参数能放对象引用吗这是新手最容易犯的错误之一。事件参数里放了对象引用然后在别的处理器里修改了它最后发现所有读到这个引用的地方数据都不对了。前面我说过参数结构体推荐用结构体并且尽量放值类型字段但业务上往往也绕不开放对象引用。放引用本身不违规但必须遵守两条纪律。第一事件参数只用于读取不要在事件处理流程中修改引用指向的对象状态第二如果你确实需要通过事件让某个处理器修改对象状态那么这个操作应该是幂等的不能因为事件多次触发就重复应用。比如通过事件给玩家加金币如果事件被重复派发了两次金币就重复加了这就是Bug。黄金法则是事件参数是快照而非指针。所有需要跨模块传递的、可变的业务数据不要靠事件参数来传而是通过查询组件或者传Id后由处理器自己去取。比如这样传public struct UnitDeadEvent { public long UnitId { get; set; } public long AttackerId { get; set; } public long Damage { get; set; } }处理器内部通过UnitId再去EntityComponent里查Unit对象得到的一定是当前最新的状态。这种做法虽然多了一次查询但彻底避免了数据脏读问题。4.3 在热更新场景下如何管理事件监听ET8.1支持热更新但热更新代码和主程序的事件扫描有个细节要注意热更新程序集是在运行时动态加载的CodeTypes的初始化扫描发生在加载之前所以热更新程序集里的Handler类默认不会被扫描注册。解决的做法是在热更新模块加载完成后手动注册事件。ET框架的代码热更新方案里通常都有一个入口方法在里面做手动注册public static class HotfixEntry { public static void Init() { EventSystem eventSystem Game.Scene.GetComponentEventSystem(); // 注册热更新程序集里的事件处理器 eventSystem.RegisterEvent(EventType.UnitDead, typeof(HotfixUnitDeadHandler)); eventSystem.RegisterEvent(EventType.UnitLevelUp, typeof(HotfixUnitLevelUpHandler)); } public static void Destroy() { EventSystem eventSystem Game.Scene.GetComponentEventSystem(); // 反注册 eventSystem.UnRegisterEvent(EventType.UnitDead, typeof(HotfixUnitDeadHandler)); eventSystem.UnRegisterEvent(EventType.UnitLevelUp, typeof(HotfixUnitLevelUpHandler)); } }这件事在热更新方案里必须做而且要注意顺序先反注册旧的事件处理器再注册新的防止热更新后同一事件同时被旧逻辑和新逻辑各执行一遍导致逻辑重复。4.4 事件驱动架构在项目里怎么划分边界把事件机制用到项目里不是简单地发布订阅就完事了它对你的架构思路有要求。我在几个项目里摸爬滚打后的体会是事件机制最适合做跨模块通知不适合做业务请求。什么叫跨模块通知就是某个状态变化了、某个行为发生了需要让其他有兴趣的模块知道。比如登录完成背包变化任务进度更新玩家升级这是通知适合事件。什么叫业务请求就是A模块要求B模块执行一个动作并且返回结果。比如请求玩家数据请求扣减货币这种是请求调用应该用RPC或者方法调用不能用事件。如果用事件去做请求你会面临巨大的调试和分析困难——你发出一个请求事件但不知道谁会处理也不知道什么时候处理完所有时序都无法确定。这是一条很值得刻在工位上的原则事件是广播不是对话。弄清这个边界事件监听才能给你带来架构上的红利否则只是把代码耦合从明处搬到了暗处问题更麻烦。5. 常见问题与排查技巧实录5.1 事件不触发的排查思路这是所有ET开发碰到最多的问题代码写完了、事件发布了但处理器里的逻辑就是不走。按我踩坑的经验你按下面这个顺序排查一次性解决百分之九十的问题。第一检查事件类型字符串是否一致。EventType.UnitDead是静态字符串常量如果你在Event特性里手写成了UnitDeath或者UnitDead 带了个空格就永远匹配不上。这种低级错误在新手里出得最多排查方法最简单发布的地方和监听的地方打日志看看两边的事件名字符串是不是一字不差。第二检查Handler类所在的程序集有没有被扫描到。ET8.1的CodeTypes扫描程序集是有一套白名单机制的如果你的Handler不小心放在了一个没有被扫描到的程序集里那框架根本不知道这个类的存在。检查方法在Handler的构造函数里打日志如果启动时没有输出就是没被扫描到。第三确认事件参数类型是不是匹配。AEvent的泛型参数必须和Publish传入的参数类型完全一致如果在AEventUnitDeadEvent里传入了另一个结构体类型框架在反射调用的时候会抛异常但有时候异常被系统吞了就会表现为事件不触发。第四检查是不是在事件处理器内部出现了未捕获的异常。ET8.1的事件系统内部对处理器执行做了异常捕获默认情况下某个处理器抛异常不会导致整个发布流程中断但那个处理器之后的逻辑就不执行了。如果你发现前面的处理器执行了、后面的没执行多半是中间某个处理器抛异常了。去日志里翻异常栈。5.2 Handler内部异常为什么会静默丢失这是我最近被问得比较多的问题值得单独拿出来讲。ET8.1的EventSystem在派发事件时如果处理器内发生了没有被捕获的异常这个异常会被框架捕获并写入日志。但默认的日志级别可能不会把它当作错误输出或者日志文件过于庞大你很难一眼翻到。所以我的习惯是在事件的PublishAsync外层再加一层统一的try-catch在关键业务事件上打更明显的错误日志try { EventSystem eventSystem Game.Scene.GetComponentEventSystem(); await eventSystem.PublishAsync(new UnitDeadEvent { UnitId target.Id, AttackerId attacker.Id, Damage lastDamage }); } catch (Exception e) { Log.Error($UnitDead事件派发失败, UnitId: {target.Id}, 异常: {e}); }这里还有一个容易被坑到的点如果是Publish同步派发方式同步方法内部不会等待Handler的异步部分完成。也就是说Publish返回的那一刻可能有Handler还没跑完异常也是在那之后抛出的你外层try-catch根本拦不住。这种异步异常问题需要靠框架的全局异常捕获机制去接住或者在实际项目中规规矩矩用PublishAsync并真正await它。5.3 内存泄漏与重复注册问题事件处理器会不会导致内存泄漏理论上不会因为Handler由框架统一管理不随业务对象销毁而销毁。但实际项目中我会遇到一种假的内存泄漏——同一事件被重复注册了。什么情况下会重复注册热更新重载逻辑时没有反注册旧的事件或者手动RegisterEvent被调用了多次。重复注册后发布一次事件处理器会被调用两次业务上就出现双倍扣款、双倍奖励这类严重Bug。排查方法很简单在Handler的Run方法开头打个日志看连续两次触发时是不是同一个处理器被打印了两遍。如果是就是重复注册。解决方法是把注册和反注册成对出现。尤其是在热更新里Destroy方法必须把Init里注册的事件全部反注册干净一个都不能漏。我在ET8.1的热更新方案里专门维护了一个注册表每次手动注册都把事件名和处理器类型记录到列表里销毁时遍历反注册从机制上避免漏掉的注册。5.4 事件参数修改了却没有生效检查是不是结构体拷贝C#结构体是值类型传递时会拷贝一份。事件发布时你用对象初始化器创建了一个结构体实例PublishAsync接收这个结构体时是值拷贝处理器接收的也是结构体拷贝。也就是说如果在处理器里修改参数结构体的字段修改的是本地拷贝对发布者和其它处理器都没影响。这个特性在ET的事件系统里其实很有用——它保证了事件参数天然是隔离的但如果你以为事件参数是引用传参就会困惑我改了为什么没生效。我之前见过一个同事在Handler里把args.Damage改了期望后面的处理器读到修改后的值结果发现其他处理器读到的还是原值。这就是没搞清结构体的值语义。如果要跨多个处理器共享一份可修改的数据正确的做法是把这个数据放到一个独立对象里然后在事件参数里传这个对象的引用。5.5 性能问题事件太多会拖垮逻辑主线程吗事件机制用多了之后自然会担心性能。我在压测项目中统计过一个高实时性的战斗场景里每秒钟可能会发布几百个事件每个事件的处理器数量在几个到十几个不等累积下来的调用量还是不小的。ET8.1的事件派发性能在同类框架里算是好的但再好的框架也架不住你无脑在每帧里发布大量事件。我总结了几条实际的性能优化建议。第一事件参数里不要放大的集合对象。如果你的事件参数结构体里塞了一个ListItem每一次发布都要构建并拷贝这个List内存和耗时都会很可观。大数据的传递应该传Id让处理器自己查询。第二避免在事件处理器里做重的IO操作。事件处理器是在逻辑主线程执行的Publish同步调用时如果你在里面写文件、发大网络包、算复杂的寻路整个游戏逻辑都会卡住别的事件。重操作应该拆出来放到专门的线程或队列里。第三如果可以将高频低逻辑事件合并。比如玩家移动过程中位置变化这种事件可能一秒钟触发几十次如果每个像素点都发事件再轻的事件框架开销也架不住。合理设置节流策略或者减少事件频率才是治本的方法。我在压测中还发现一个有意思的点事件处理器本身的开销远小于事件参数结构体构建的开销。因为参数构建是在发布方完成的它会在栈上分配和拷贝。参数越复杂这个开销越大。所以参数结构体保持精简不仅是为了代码清晰也是为了性能。5.6 常见问题速查表问题现象可能原因解决办法事件完全不触发事件类型字符串不一致对比发布和监听两边的EventType字符串事件完全不触发Handler所在程序集未被扫描检查程序集引用和CodeTypes扫描配置事件完全不触发参数泛型类型不匹配确认AEventT和Publish参数类型一致处理器只执行了一部分中间某个处理器抛异常逐个处理器try-catch翻完整异常日志事件触发了多次热更新重复注册检查RegisterEvent/UnRegisterEvent是否成对参数改了但其他处理器无感知结构体值拷贝语义用引用类型包装共享可变数据事件发布卡主线程处理器内有重IO/重计算拆分重操作用额外线程或队列事件发布后依赖的处理器未执行完用了同步Publish改用PublishAsync并await6. 关于事件监听的几条实战心得这些东西可能不算什么高深技术但都是实际项目里堆出来的经验。事件监听在ET8.1里的正确姿势其实可以浓缩成几个关键词解耦、快照、异步、规范。解耦是目的快照是传参原则异步是处理器的天然形态规范是团队协作的底线。我个人的建议是在你的项目里尽早定好事件命名规范、事件参数规范、Handler编写规范并把这些规范写进团队文档。不要等到项目代码多了、事件泛滥了再回头整理那时候改造成本已经非常高了。关于事件和普通方法调用的取舍我的经验基准线是这样的同一个模块内部的方法调用用直接调用跨模块、且调用方不关心被调用方具体做什么的用事件跨模块且调用方需要被调用方返回结果的用RPC或方法调用。最后一条尤其重要不要让事件监听变成隐式的方法调用否则代码会变得极难追踪。另外在事件链路里加追踪ID是一个非常值得做的工程实践。每条事件发布时可以给它分配一个自增ID然后所有Handler的日志里都带上这个ID。这样线上排查问题的时候一条事件的完整处理链路就能快速串起来极大提升排查效率。我在几个中大型项目里用了这个方案效果立竿见影。ET8.1的事件系统并不复杂但它是整个框架的中枢神经系统。用好了你的项目模块边界会非常清晰用不好事件穿透到各个角落查找逻辑会变成一场噩梦。希望这篇东西能帮你在ET的事件监听上少走一些弯路。如果你在项目里也遇到过更隐蔽的事件坑欢迎单独交流这类问题光靠想是想不出来的都是踩出来的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价