发布订阅模式,很多地方都用到的一种模式,简单的说就是预定一件事情,时机成熟通知你,比如我们nodejs中的fs的读写文件的流,消息事件的触发等都用到了这种方式了,虽然不是特别难,但是非常实用,实现方式也简单,基本思想就是内部保存了一个对象存储订阅的函数,调用者通过名字来触发函数,订阅多个就按照队列的形式触发。
DOM 的事件机制就是发布订阅模式最常见的实现,这大概是前端最常用的编程模型了,监听某事件,当该事件发生时,监听该事件的监听函数被调用。
class EventEmitter {
constructor() {
this.events = Object.create(null)
}
on(type, handler) {
;(this.events[type] || (this.events[type] = [])).push(handler)
}
off(type, handler) {
if (this.events[type]) {
this.events[type].splice(this.events[type].indexOf(handler) >>> 0, 1)
}
}
emit(type) {
let args = [].slice.call(arguments, 1)
let array = this.events[type] || []
array.forEach(cb => {
cb.apply(this, args)
})
}
once(type, handler) {
function _fn() {
handler.apply(this, arguments)
this.off(type, _fn)
}
this.on(type, _fn)
}
}
export default EventEmitter
使用
let em = new EventEmitter()
function fn(price) {
console.log('price', price)
}
em.once('work', fn)
em.off('work', fn)
em.emit('work', 100)
console.log(em)
在 Vue 中不同组件之间通讯,有一种解决方案叫Event Bus,这其实就是发布订阅模式的实现,非常简单好用。
在正式开始之前我们先来说说数据绑定的事情,数据绑定我的理解就是让数据M(model)展示到 视图V(view)上。我们常见的架构模式有 MVC、MVP、MVVM模式,目前前端框架基本上都是采用 MVVM 模式实现双向绑定,Vue 自然也不例外。但是各个框架实现双向绑定的方法略有所不同,目前大概有三种实现方式。
而 Vue 则采用的是数据劫持与发布订阅相结合的方式实现双向绑定,数据劫持主要通过 Object.defineProperty
来实现。
所谓MVVM数据双向绑定,即主要是:数据变化更新视图,视图变化更新数据.
实现Vue的数据双向绑定,需要如下:
总结:
实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer
和订阅者Watcher
之间进行统一管理的。
这个有问题,没有传参数, 需要自行修改一下
// 发布订阅模式
class EventEmitter {
constructor() {
// 事件对象,存放订阅的名字和事件
this.events = {
};
}
// 订阅事件的方法
on(eventName,callback) {
if (!this.events[eventName]) {
// 注意时数据,一个名字可以订阅多个事件函数
this.events[eventName] = [callback]
} else {
// 存在则push到指定数组的尾部保存
this.events[eventName].push(callback)
}
}
// 触发事件的方法
emit(eventName) {
// 遍历执行所有订阅的事件
this.events[eventName] && this.events[eventName].forEach(cb => cb());
}
// 移除订阅事件
removeListener(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb != callback)
}
}
// 只执行一次订阅的事件,然后移除
once(eventName,callback) {
// 绑定的时fn, 执行的时候会触发fn函数
let fn = () => {
callback(); // fn函数中调用原有的callback
this.removeListener(eventName,fn); // 删除fn, 再次执行的时候之后执行一次
}
this.on(eventName,fn)
}
}
使用方式
let em = new EventEmitter();
let workday = 0;
em.on("work", function() {
workday++;
console.log("work everyday");
});
em.once("love", function() {
console.log("just love you");
});
function makeMoney() {
console.log("make one million money");
}
em.on("money",makeMoney);
let time = setInterval(() => {
em.emit("work");
em.removeListener("money",makeMoney);
em.emit("money");
em.emit("love");
if (workday === 5) {
console.log("have a rest")
clearInterval(time);
}
}, 1);
推荐
// source code
all = all || Object.create(null);
Object.create(null)
:生成的对象是一个原型为空的对象。节约内存且避免冲突,因为没有原型,且普通对象原型上的属性和方法也相应没有了。
// source code
(all[type] || (all[type] = [])).push(handler);
// my code - bad
if (all[type]) {
all[type].push(handler)
} else {
all[type] = [handler]
}
简洁的队列赋值:短路逻辑判断 + 初始化 + 更新数组,简直不要太优雅。
// source code
all[type].splice(all[type].indexOf(handler) >>> 0, 1);
~
,可以结合.indexOf()
使用,因为对任一数值 x 进行按位非操作的结果为 -(x + 1)
,即:~-1 = 0
// source code
(all[type] || []).slice().map((handler) => {
handler(evt); });
(all['*'] || []).slice().map((handler) => {
handler(type, evt); });
// source code
!!(0) // false
!!(null) // false
!!('') // false
!!(undefined) // false
!!(NaN) // false
!!(2) // true
+val
将字符串数字转为数字。如果 val 是非字符串数字,则 +val = NaN // source code
+'123456' // 123456, Number
+new Date() // 1527684413484, 相当于 new Date().getTime()
TypeScript/理解Event Emitter (事件派发器)推荐
Understanding Event Emitters
上面案例的github代码
一个例子 - 看尽并手写JS发布订阅模式
前端必懂EventEmitter,不懂会丢人