优秀的编程知识分享平台

网站首页 > 技术文章 正文

事件驱动、事件循环、事件代理都是什么你弄得清吗

nanyue 2024-10-18 07:40:15 技术文章 9 ℃

目前最常用到的概念就是事件。绝大多数项目都是离不开这个基本技巧的。事件本身提供了一种通信的可能,让一个子系统可以第一时间得知外界的输入,同时和 API 调用非常不同的是,它可以让事件的发送者和监听者互不认识。这样可以非常有效的解除系统间的耦合。当然这种低耦合只是设计层面。这个概念和事件驱动设计并不是一回事,但有相当大的交集。

事件驱动与事件循环

如果谈到事件驱动设计的话,它指的是相对于流式设计,程序的流动取决于用户输入而不是预先编好的顺序。他关注的是程序的时序。前端工程绝大多数情况都满足这个范式,他往往是在和用户之间互动。即使放弃 OOP 的传统流开发也极少能完全杜绝跟用户输入交互改变程序流向。

包括 JavaScript 在内典型的事件驱动设计实现方式是 event loop,用一个定时循环去读输入状态的变化,不间断的监听循环会在发生输入变化时通知监听器。

要知道操作系统级别的微机原理上,CPU 永远是在不间断的执行下一条指令。对微机来说没有可能让程序“等待”在“闲置”状态(只有操作系统根据更底层的用户输入调度来模拟,挂起一个进程)。程序本身所有的等待都是靠空循环来判断使等待结束的变化发生的。因为计算机的运行本身就是流。

非常“极端”地设计得让所有程序流程都靠 event loop 以事件为起点,就是事件驱动设计模式。这也给了 Node.js 非常的优势。一个 Node.js 应用会在 event loop 里不断寻找事件,找到后就去执行它。所有异步工作都在一个线程内完成。除了 Erlang 再没有其他语言能以这种不新建线程的方式大规模的处理异步工作(Erlang 自己模拟了一种微-‘伪’划掉- 线程)。

不过我们开发前端工程时写的事件驱动,一般是指的在系统的用户事件之外的通信,除了系统提供的用户事件,各个模块、组件之间的通信也用事件来完成。事件除了用户输入产生的也可以是模块自发的。

不用事件循环时的情况

事件循环是面向底层执行的设计技巧。我们会愿意用一个空循环来挨个比对所有事件所有值吗?用高级语言写应用时用 cs 指针队列的方式思考很荒谬。如果事件调用不用事件循环,那么事件的触发者需要访问到回调队列。当事件发生时,主动地去通知代理程序或者队列。

在像 JavaScript 这类带有 GC 的高级语言来说,即使相对先进的 Mark and Sweep 方案的 GC,事件监听的后果是无论何种方式实现也必须手动回收。因为事件的发送者即使在设计模式上不用知晓监听者,实际上必须抱有对一切监听者的引用。这实际上相当于 OOP 之中的代理概念,只是通过事件包装让工程师用户体验略有不同。

当 OOP 的一个监听者主体消亡之后,我们必须显式的清除掉他对被监听者的注册,必须设计一个析构函数来移除所有注册出去的监听。或者,在被监听者的代理遍历时,去主动识别监听者的状态,或者其他指定的时刻手动的移除掉无效的监听者。在这个意义上,如果不通过 JS 框架来协助开发者,那么事件的一大意义:发出者不需要知道监听者的概念并不能真的实现。

我们来看看 Node.js 的 events 库是怎么实现的https://github.com/nodejs/node/blob/master/lib/events.js

_events 对象储存事件 map

毫不意外是一个代理队列。

map 中的 handler 队列挨个同步执行

Node.js 的文档里介绍 fs 或者 server 库的事件用的都是这个对象。尽管毫无疑问这几类底层一定是 event loop。

事件循环提供的另一个便利是,新注册者出现时,如果我们愿意,可以发送给新注册者事件监控器当前读到的 old value。也就是说一个事件驱动系统里面监听者的生命周期开始时,我们有初始值可以得到。

如果是代理方式我们只有等到下一次事件发生才能拿到一个值了。在这种需要初始值的时候,监听者仿佛也并不能不去认识发送者。

这时又回到了问题的本质,有这种需要的时候似乎不一定要用事件抽象。毕竟事件在我们用 JavaScript 写的底层代码里仅仅是 delegate 而已。


代理 delegate 和委托模式 delegation partten

代理是 OOP 五花八门的设计模式里比较重量级的一个。形式化的定义绕嘴又难懂。简单的说(过分简单地),就是让一个类去决定另一个类的事情发生。

比如小 A 有吃饭的功能和下班的功能。小 A 把下班代理给男朋友 B。当 A 执行下班时,B 带她去吃饭。

Class A {

delegate B b;

private eat (Food f) { print("I got fatter");}

private getoff (Work w) { b.getoff(w); }

}

Class B {

getoff (Work w) { this.eat(BigMac); }

eat (Food f ) { print('Who get fat?'); }

}

// 输出:I got fatter;

因此也有人把这种设计模式说成 self 延迟绑定模式。

这个例子里的代理可以非常简单的改成继承的方式实现。但是对可读性,为每一个代理执行一个继承是一种非常病态的设计。

改继承为代理就是委托模式。委托模式极大的解放了继承,把类关系组织成了立体的网而不是一圈套一圈的一维结构。

广义上说,只要一个类的成员调用由另一个类启动,这就是代理。事件的回调显然是回调所有者对于事件发送者的代理。

所以我们写的程序到底是事件驱动还是委托模式呢。事件驱动至少是前端工程的必然结果。委托模式是实现事件的方案。我们设计的事件高聚合,更多的应用时偏向于代理,但统一包装成了事件驱动。

更重要的是我们应该时常反思习惯,追求本质。把代理写成代理、事件写成事件、属性写成属性都很重要。把信号一律描述成事件不仅不是事件驱动,还可能增加成本。

最近发表
标签列表