一、前言
mini-signals 是一款轻量、快速的发布-订阅模式库。对于一名合格的前端仔,应该对发布-订阅模式了然于胸。
❝
发布-订阅模式定义了对象间一对多的依赖关系,可以用来解决对象间耦合的问题。
❞
从 DOM 的事件系统,到 Node.js 的 events 模块,再到目前流行的 MVVM 架构模式,发布-订阅模式无处不在。
利用 JavaScript 的一些基础知识,可以快速地实现一个简易的发布-订阅模式:
function EventEmitter () {
this.events = {};
}
EventEmitter.prototype.on = function (event, fn) {
const { events } = this;
if (!events[event]) {
events[event] = [];
}
events[event].push(fn);
}
EventEmitter.prototype.emit = function (event, ...args) {
const { events } = this;
const listeners = events[event];
if (!listeners || listeners.length === 0) {
return;
}
for (let i = 0; i < listeners.length; i++) {
listeners[i].apply(this, args);
}
}
复制代码
那么 mini-signals 到底与大部分的发布-订阅模式的实现有什么区别呢?接下来本文带你一步步揭晓。
二、基础使用
探索源码之前,首先需要了解 mini-signals 的使用方式:
const signal = new MiniSignals();
signal.add(task);
signal.dispatch('foo', 'bar');
function task(foo, bar) {
// do something
}
复制代码
而大部分同学习惯的使用方式如下:
const events = new EventEitter();
events.on('eventname', task);
events.emit('eventname', 'foo', 'bar');
复制代码
单从使用方式来看,mini-signals 有如下优势:
- 不需要通过额外的常量来管理发布-订阅的事件名。
- 事件不再通过一条总线管理,一个实例对应一个独立的事件容器,一定程度上减少了程序的复杂度。
三、双向链表管理事件监听
mini-signals 并没有直接在 MiniSignal 对象上创建一个数组来管理事件监听。
class MiniSignal {
constructor () {
// 前驱和后继
this._head = this._tail = undefined;
}
}
复制代码
而是选择实现一个朴素的「双向链表」来管理事件容器上的事件监听,由于双向链表的节点需要记录其前驱和后继节点,所以需要 MiniSignalBinding 类来创建节点实例:
class MiniSignalBinding {
constructor (fn, once = false, thisArg) {
this._fn = fn;
// 回调函数是否只执行一次
this._once = once;
this._thisArg = thisArg;
// 记录当前节点的前驱和后继
this._next = this._prev = null;
// 归属的事件容器
this._owner = null;
}
// 解除绑定方法
detach () {
if (this._owner === null) return false;
this._owner.detach(this);
return true;
}
}
复制代码
MiniSignalBinding 内部通过 _prev 和 _next 来记录前驱和后继节点,并且通过 _owner 属性来记录当前节点归属的事件容器。
这一套操作还是比较专业的。
四、注册事件监听
MiniSignal 提供 add 方法来注册事件监听:
MiniSignal.prototype.add = function (fn, thisArg = null) {
if (typeof fn !== 'function') {
throw new Error('MiniSignal#add(): First arg must be a Function.');
}
return _addMiniSignalBinding(this, new MiniSignalBinding(fn, false, thisArg));
}
复制代码
创建好节点对象后,需要更新其前驱节点、后继节点以及归属的事件容器,同时还需要更新 MiniSignal 中保存的头节点和尾节点:
function _addMiniSignalBinding (self, node) {
if (!self._head) {
self._head = node;
self._tail = node;
} else {
self._tail._next = node;
node._prev = self._tail;
self._tail = node;
}
node._owner = self;
return node;
}
复制代码
五、派发事件
MiniSignal 派发事件时,需要实现遍历链表的操作,然后执行保存在节点中的回调函数即可:
dispatch () {
let node = this._head;
if (!node) return false;
while (node) {
if (node._once) this.detach(node);
node._fn.apply(node._thisArg, arguments);
node = node._next;
}
return true;
}
复制代码
另外对于只需要执行一次的事件监听,需要同时进行解绑操作。
六、解绑事件监听
解绑操作需要实现链表的删除操作,更新前驱节点的后继指针和后继节点的前驱指针:
detach (node) {
if (!(node instanceof MiniSignalBinding)) {
throw new Error('MiniSignal#detach(): First arg must be a MiniSignalBinding object.');
}
if (node._owner !== this) return this;
if (node._prev) node._prev._next = node._next;
if (node._next) node._next._prev = node._prev;
if (node === this._head) {
this._head = node._next;
if (node._next === null) {
this._tail = null;
}
} else if (node === this._tail) {
this._tail = node._prev;
this._tail._next = null;
}
node._owner = null;
return this;
}
复制代码
七、总结
以上便是 mini-signals 库的核心代码,非常的轻量。
同时也可以看出 MiniSignal 相比较大部分的发布-订阅模式的实现还是有一些优势的:
- 利用双向链表管理事件监听,从而引入了单独的对象来维护每一个事件监听函数,提供了程序的可读性。
- 单独的事件容器,相比较事件名的约定,不需要再维护大量的常量事件名,一定程度上减少了程序的复杂度,提高了程序的可维护性。
以上就是本文的全部内容,希望能够给你带来帮助,欢迎「关注」、「点赞」、「转发」。