概述

在前端里面感觉有很多类库或者模块都用到了这个思想,比如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 移除监听

我们先来实现onemit这两个方法,我们看上述的例子,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的方法
//可以看到确实只监听了一次