概述
在前端里面感觉有很多类库或者模块都用到了这个思想,比如Vue的数据绑定、Vue里面的Bus总线通信、Node.js的Event类(Event类作为一个基类,很多类都继承了他)。
实现一个简单的发布订阅
我们新建一个index.js
,先填入一些方法。首先我们要有两个类,一个是发布者类Pub
,一个是订阅者类Sub
。
订阅者类里面应该保存有订阅者的信息,这里为了简便只保存一个name
,当然少不了的是订阅别人的方法,订阅这个动作是订阅者发出的,所以它应该属于订阅者的类方法。发布者类里应该有一个数组来记录所有的订阅者,然后还要有一个发布方法。
//订阅者 function Sub(name){ this.name = name || '游客' this.subscribe = function(){ //something } } function Pub(){ this.subcribers = [] this.publish = function(){ //something } }
然后我们不急着去补全这些方法,先把我们发布订阅这个逻辑全部走完。
//实例化一个发布者 let peopleNewspaper = new Pub() //实例化两个订阅者 let zhangsan = new Sub('张三') let lisi = new Sub('李四') //订阅操作 zhangsan.subscribe(peopleNewspaper) lisi.subscribe(peopleNewspaper)
订阅者调用了自身的subscribe
方法去订阅一个发布者,实际上我们要做的是,把这个订阅者添加到发布者的subcribers
数组里,所以我们可以这样写
function Sub(name){ ``` this.subscribe = function(pub){ pub.subscribers.push(this) } }
this
指向的就是调用方法的对象,也就是实例zhangsan lisi,但是这样写还有点不太好,订阅者发出订阅动作之后,发布者应该知道是谁订阅了他,所以这边可以这样改一下
function Sub(name){ ``` this.subscribe = function(pub){ pub.add(this) } } function Pub(){ ``` this.add = function(sub){ this.subscribers.push(sub) //这里只是模拟 通知发布者有新的订阅者订阅,实际上所进行的操作视业务场景而定 console.log(`${sub.name}订阅了你`) } }
然后要完成的就是发布代码,直接上代码吧
function Pub() { ``` this.publish = function (value) { this.subscribers.forEach(item => { item.notify(value) }) } } function Sub(name) { ``` this.notify = function (value) { console.log(`您好,${this.name}!您有新的报纸--${value}`) } }
完整代码如下
//订阅者 function Sub(name) { this.name = name || '游客' this.subscribe = function (pub) { pub.add(this) } this.notify = function (value) { console.log(`您好,${this.name}!您有新的报纸--${value}`) } } //发布者 function Pub() { this.subscribers = [] //订阅者订阅发布者之后 发布者应该会收到消息 this.add = function (sub) { this.subscribers.push(sub) console.log(`${sub.name}订阅了你`) } this.publish = function (value) { this.subscribers.forEach(item => { item.notify(value) }) } } //实例化一个发布者 let peopleNewspaper = new Pub() //实例化两个订阅者 let zhangsan = new Sub('张三') let lisi = new Sub('李四') //订阅操作 zhangsan.subscribe(peopleNewspaper) //张三订阅了你 lisi.subscribe(peopleNewspaper) //李四订阅了你 //发布者发布新消息 setTimeout(() => { peopleNewspaper.publish('人民日报') //您好,张三!您有新的报纸--人民日报 您好,李四!您有新的报纸--人民日报 }, 2000)
手写简单的Event类
先用一个简单的例子来说明Node.js里面的EventEmitter是如何使用的
var events = require('events'); var emitter = new events.EventEmitter(); emitter.on('event1', function(arg1, arg2) { console.log('listener1', arg1, arg2); }); emitter.on('some_event', function() { console.log('some_event 事件触发'); }); setTimeout(function() { emitter.emit('some_event'); }, 1000); emitter.emit('event1', 'arg1 参数', 'arg2 参数'); //控制台输出 // listener1 arg1 参数 arg2 参数 // some_event 事件触发
这个类包含4个方法,分别是
- on 监听一个事件,并触发回调函数
- emit 发送一个事件
- once 监听一次就移除
- off 移除监听
我们先来实现on
、emit
这两个方法,我们看上述的例子,on
方法第一个参数应该是事件类型,而往后的参数是一些回调函数(可以不止一个回调函数),而emit
方法第一个参数也是事件类型,往后的参数是传给对应的on
方法的回调函数的参数。
//event.js function Event(){ this.events = [] this.eventsType = [] //存储事件类型 方便后续遍历emit的类型是否已经监听 this.on = function(...event){ //简单的参数校验 if (!event[0]) { throw new Error('事件类型不可以为空') } let _event = {} _event.type = event[0] //事件类型 _event.fn = event.slice(1) //事件回调函数 this.events.push(_event) this.eventsType.push(event[0]) //另外存储事件类型,方便后续比较 } this.emit = function(type,...args){ //简单的参数校验 if (this.eventsType.indexOf(type) === -1) { throw new Error(`不存在监听${type}的方法`) } //遍历events数组,寻找相对应的type this.events.forEach(item => { if (item.type === type) { //调用所有回调函数 item.fn.forEach(fn => { fn.call(this, ...args) }) } }) } }
接下来来实现off
方法和once
方法,off
方法实现思路比较清晰,直接把对应的事件类型从events
数组里面移除就好了。
this.off = function (type) { let index = this.eventsType.indexOf(type) if (index === -1) { throw new Error(`不存在${type}事件,无法移除`) } else { this.eventsType.splice(index, 1) this.events.forEach((item, index) => { if (item.type === type) { this.events.splice(index, 1) } }) } }
测试如下
let event = new Event() event.on('hello', function (name) { console.log(`hello,${name}!`) }, function () { console.log(1) }) event.emit('hello', 'David') event.off('hello') event.emit('hello', 'David') //以下为控制台输出 hello,David! 1 d:\zj\ws\event.js:16 throw new Error(`不存在监听${type}的方法`) ^ Error: 不存在监听hello的方法
最后来实现once
方法,实现的思路应该是监听到一次之后就调用off
方法移除,这里要用一个flag表示一个事件是否为once类型,其余逻辑与on
方法差不多,然后emit
方法应该加上一句逻辑,emit
一次之后移除once类型的事件
this.once = function(...event){ let flag = 'once' if (!event[0]) { throw new Error('事件类型不可以为空') } let _event = {} _event.type = event[0] _event.fn = event.slice(1) _event.flag = flag this.events.push(_event) this.eventsType.push(event[0]) }
emit
方法应该加上以下一句
if (item.type === type) { item.fn.forEach(fn => { fn.call(this, ...args) }) //once方法emit一次后应该被移除 if(item.flag === 'once') this.off(item.type) }
测试如下
let event = new Event() event.once('hello', function (name) { console.log(`hello,${name}!`) }, function () { console.log(1) }) event.emit('hello', 'David') event.emit('hello', 'David') //以下为控制台输出 hello,David! 1 d:\zj\ws\event.js:16 throw new Error(`不存在监听${type}的方法`) ^ Error: 不存在监听hello的方法 //可以看到确实只监听了一次