先来个最简单的例子:
let corp = {}; // 自定义一个公司对象
// 这里放一个列表用来缓存回调函数
corp.list = [];
// 去订阅事件
corp.on = function (fn) {
// 二话不说,直接把fn先存到列表中
this.list.push(fn);
};
// 发布事件
corp.emit = function () {
// 当发布的时候再把列表里存的函数依次执行
this.list.forEach(cb => {
cb.apply(this, arguments);
});
};
// 测试用例
corp.on(function (position, salary) {
console.log('你的职位是:' + position);
console.log('期望薪水:' + salary);
});
corp.on(function (skill, hobby) {
console.log('你的技能有: ' + skill);
console.log('爱好: ' + hobby);
});
corp.emit('前端', 10000);
corp.emit('端茶和倒水', '足球');
/*
你的职位是:前端
期望薪水:10000
你的技能有: 前端
爱好: 10000
你的职位是:端茶和倒水
期望薪水:足球
你的技能有: 端茶和倒水
爱好: 足球
*/
上面通过自定义事件实现了一个简单的发布订阅模式,不过从打印出来的结果来看,有点小尴尬。Why?
因为在正常的情况下,希望打印的是酱紫的:
/*
你的职位是:前端
期望薪水:10000
你的技能有: 端茶和倒水
爱好: 足球
*/
之所以出现此种情况,那是在on方法的时候一股脑的都将fn函数全部放到了列表中。然而需要的只是一个简单的key值,就可以解决了。让我们改写一下上面的代码
let corp = {};
// 这次换成一个对象类型的缓存列表
corp.list = {};
corp.on = function(key, fn) {
// 如果对象中没有对应的key值
// 也就是说明没有订阅过
// 那就给key创建个缓存列表
if (!this.list[key]) {
this.list[key] = [];
}
// 把函数添加到对应key的缓存列表里
this.list[key].push(fn);
};
corp.emit = function() {
// 第一个参数是对应的key值
// 直接用数组的shift方法取出
let key = [].shift.call(arguments),
fns = this.list[key];
// 如果缓存列表里没有函数就返回false
if (!fns || fns.length === 0) {
return false;
}
// 遍历key值对应的缓存列表
// 依次执行函数的方法
fns.forEach(fn => {
fn.apply(this, arguments);
});
};
// 测试用例
corp.on('join', (position, salary) => {
console.log('你的职位是:' + position);
console.log('期望薪水:' + salary);
});
corp.on('other', (skill, hobby) => {
console.log('你的技能有: ' + skill);
console.log('爱好: ' + hobby);
});
corp.emit('join', '前端', 10000);
corp.emit('join', '后端', 10000);
corp.emit('other', '端茶和倒水', '足球');
/*
你的职位是:前端
期望薪水:10000
你的职位是:后端
期望薪水:10000
你的技能有: 端茶和倒水
爱好: 足球
*/
来个通用的
现在来搞个通用的发布订阅模式实现,和刚才的差不多,不过这次起名也要隆重些了,直接叫event吧,看代码
let event = {
list: {},
on(key, fn) {
if (!this.list[key]) {
this.list[key] = [];
}
this.list[key].push(fn);
},
emit() {
let key = [].shift.call(arguments),
fns = this.list[key];
if (!fns || fns.length === 0) {
return false;
}
fns.forEach(fn => {
fn.apply(this, arguments);
});
},
remove(key, fn) {
// 这回我们加入了取消订阅的方法
let fns = this.list[key];
// 如果缓存列表中没有函数,返回false
if (!fns) return false;
// 如果没有传对应函数的话
// 就会将key值对应缓存列表中的函数都清空掉
if (!fn) {
fns && (fns.length = 0);
} else {
// 遍历缓存列表,看看传入的fn与哪个函数相同
// 如果相同就直接从缓存列表中删掉即可
fns.forEach((cb, i) => {
if (cb === fn) {
fns.splice(i, 1);
}
});
}
}
};
function cat() {
console.log('一起喵喵喵');
}
function dog() {
console.log('一起旺旺旺');
}
event.on('pet', data => {
console.log('接收数据');
console.log(data);
});
event.on('pet', cat);
event.on('pet', dog);
// 取消dog方法的订阅
event.remove('pet', dog);
// 发布
event.emit('pet', ['二哈', '波斯猫']);
/*
接收数据
[ '二哈', '波斯猫' ]
一起喵喵喵
*/
这样其实就实现了一个可以使用的发布订阅模式了,其实说起来也是比较简单的,来一起屡屡思路吧
思路:
-
创建一个对象(缓存列表)
-
on方法用来把回调函数fn都加到缓存列表中
-
emit方法取到arguments里第一个当做key,根据key值去执行对应缓存列表中的函数
-
remove方法可以根据key值取消订阅
工作中的应用
插广告
先给大家看一个,在这个新闻转码页的项目中,我负责写下面推荐流的内容(就是喜欢的人还看了那里)。如下图所示
圈起来的广告部分,这里并不是我来负责的,需要另外一个负责对接广告业务的大牛来实现的。那么,他想要在我的推荐流中插入广告应该如何实现呢?
毕竟不能把我的代码给他,让他再拿去开发吧,这还不够费劲的呢,又要熟悉代码又要开始写广告插入的逻辑,很折腾的,时间不该这样的浪费掉
于是就用到了发布订阅模式了,我这边不需要关注广告插入的逻辑。我还是我,是颜色不一样的烟火,哈哈哈,扯远了
沟通后,我只需要把用户浏览到哪一页的page页码传给他即可。所以我只需要在我开发的代码中写一句话,利用上面实现的event来表示一下
// 省略....
render() {
// 我只在渲染的时候
// 把约定好的key和他需要的page页码传过去就可以了
event.emit('soAd', page);
}
// 省略...
node中的api=>events
/ {'失恋', [findboy, drink]}
// 监听的目的 就是为了构造这样一个对象 一对多的关系 on
// 发布的时候 会让数组的函数依次执行 emit
// [findboy, drink]
// let EventEmitter = require('events');
// 这里用接下来我们写的
let EventEmitter = require('./events');
let util = require('util');
function Girl() {
}
// Girl继承EventEmitter上的方法
util.inherits(Girl, EventEmitter); // 相当于Girl.prototype.__proto__ = EventEmitter.prototype
let girl = new Girl();
let drink = function (data) {
console.log(data);
console.log('喝酒');
};
let findboy = function () {
console.log('交友');
};
girl.on('newListener', function (eventName) {
// console.log('名称: ' + eventName);
});
girl.on('结婚', function() {});
girl.setMaxListeners(3);
console.log(girl.getMaxListeners());
girl.once('失恋', drink); // {'失恋': [drink]}
girl.once('失恋', drink); // {'失恋': [drink]}
girl.prependListener('失恋', function () {
console.log('before');
});
girl.once('失恋', drink); // {'失恋': [drink]}
girl.emit('失恋', '1');
实现一个EventEmitter
function EventEmitter() {
// 用Object.create(null)代替空对象{}
// 好处是无杂质,不继承原型链的东东
this._events = Object.create(null);
}
// 默认最多的绑定次数
EventEmitter.defaultMaxListeners = 10;
// 同on方法
EventEmitter.prototype.addListener = EventEmitter.prototype.on;
// 返回监听的事件名
EventEmitter.prototype.eventNames = function () {
return Object.keys(this._events);
};
// 设置最大监听数
EventEmitter.prototype.setMaxListeners = function (n) {
this._count = n;
};
// 返回监听数
EventEmitter.prototype.getMaxListeners = function () {
return this._count ? this._count : this.defaultMaxListeners;
};
// 监听
EventEmitter.prototype.on = function (type, cb, flag) {
// 默认值,如果没有_events的话,就给它创建一个
if (!this._events) {
this._events = Object.create(null);
}
// 不是newListener 就应该让newListener执行以下
if (type !== 'newListener') {
this._events['newListener'] && this._events['newListener'].forEach(listener => {
listener(type);
});
}
if (this._events[type]) {
// 根据传入的flag来决定是向前还是向后添加
if (flag) {
this._events[type].unshift(cb);
} else {
this._events[type].push(cb);
}
} else {
this._events[type] = [cb];
}
// 监听的事件不能超过了设置的最大监听数
if (this._events[type].length === this.getMaxListeners()) {
console.warn('警告-警告-警告');
}
};
// 向前添加
EventEmitter.prototype.prependListener = function (type, cb) {
this.on(type, cb, true);
};
EventEmitter.prototype.prependOnceListener = function (type, cb) {
this.once(type, cb, true);
};
// 监听一次
EventEmitter.prototype.once = function (type, cb, flag) {
// 先绑定,调用后删除
function wrap() {
cb(...arguments);
this.removeListener(type, wrap);
}
// 自定义属性
wrap.listen = cb;
this.on(type, wrap, flag);
};
// 删除监听类型
EventEmitter.prototype.removeListener = function (type, cb) {
if (this._events[type]) {
this._events[type] = this._events[type].filter(listener => {
return cb !== listener && cb !== listener.listen;
});
}
};
EventEmitter.prototype.removeAllListener = function () {
this._events = Object.create(null);
};
// 返回所有的监听类型
EventEmitter.prototype.listeners = function (type) {
return this._events[type];
};
// 发布
EventEmitter.prototype.emit = function (type, ...args) {
if (this._events[type]) {
this._events[type].forEach(listener => {
listener.call(this, ...args);
});
}
};
module.exports = EventEmitter;