网站首页 > 技术文章 正文
目前最常用到的概念就是事件。绝大多数项目都是离不开这个基本技巧的。事件本身提供了一种通信的可能,让一个子系统可以第一时间得知外界的输入,同时和 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 延迟绑定模式。
这个例子里的代理可以非常简单的改成继承的方式实现。但是对可读性,为每一个代理执行一个继承是一种非常病态的设计。
改继承为代理就是委托模式。委托模式极大的解放了继承,把类关系组织成了立体的网而不是一圈套一圈的一维结构。
广义上说,只要一个类的成员调用由另一个类启动,这就是代理。事件的回调显然是回调所有者对于事件发送者的代理。
所以我们写的程序到底是事件驱动还是委托模式呢。事件驱动至少是前端工程的必然结果。委托模式是实现事件的方案。我们设计的事件高聚合,更多的应用时偏向于代理,但统一包装成了事件驱动。
更重要的是我们应该时常反思习惯,追求本质。把代理写成代理、事件写成事件、属性写成属性都很重要。把信号一律描述成事件不仅不是事件驱动,还可能增加成本。
猜你喜欢
- 2024-10-18 前端反向代理(前端反向代理怎么配置)
- 2024-10-18 JavaScript 九种跨域方式实现原理
- 2024-10-18 Proxy 来代理 JavaScript 里的类(js中proxy)
- 2024-10-18 octokit.js:2023每个前端都值得学习的通用SDK!
- 2024-10-18 一面 2:JS-Web-API 知识点与高频考题解析
- 2024-10-18 使用reveal.js制作精美的网页版PPT
- 2024-10-18 es6中的代理-Proxy(es proxy)
- 2024-10-18 事件——《JS高级程序设计》(javascript高级程序设计 javascript权威指南)
- 2024-10-18 「JavaScript 从入门到精通」18.WebApi介绍
- 2024-10-18 day7:前端面试题(js)(前端面试题2021及答案js)
- 最近发表
- 标签列表
-
- cmd/c (64)
- c++中::是什么意思 (83)
- 标签用于 (65)
- 主键只能有一个吗 (66)
- c#console.writeline不显示 (75)
- pythoncase语句 (81)
- es6includes (73)
- sqlset (64)
- windowsscripthost (67)
- apt-getinstall-y (86)
- node_modules怎么生成 (76)
- chromepost (65)
- c++int转char (75)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- & (66)
- java (73)
- js数组插入 (83)
- linux删除一个文件夹 (65)
- mac安装java (72)
- eacces (67)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)