JS设计模式-观察者模式

前几天在忙着搬家,一直没有更新博客,之前在Github上看一个项目,人家用到了观察者模式,现在我把他记下来,巩固一下

观察者模式(发布/订阅模式)

对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

在现实生活中,我们经常用到这种模式,比如我在知乎上关注了某个人,或者在YouTuBe上订阅了某个频道,在微信上订阅了某个公众号,一旦我们订阅的对方发布了动态,我们就能接收到信息,这里就形成了观察者模式

我们就是一个订阅者, 而我们关注的某人/某频道/某公众号就是发布者

好了,我们就根据上面的例子来写一个简单的观察者模式:

var channel = {} //Youtube上的一个音乐频道
channel.subscribeList = [] //订阅者的列表

channel.on = function (fn) {//添加订阅这人数
    this.subscribeList.push(fn)
}

channel.emit = function () {//频道发布新的消息
    for (var i = 0; i < this.subscribeList.length; i++) {
        this.subscribeList[i].apply(this, arguments)
    }
}

channel.on(function (song) {//某人订阅了这条信息
    console.log(`今天要发布新歌是:${song.author}-${song.name},类型:${song.type}`)
})

channel.emit({//频道发布消息
    author: 'Tom Day',
    name: 'Flemington',
    type: 'Post-Rock'
})
//今天要发布新歌是:Tom Day-Flemington,类型:Post-Rock

这样,我们就完成了一个简单的观察这模式,但是,如果这个人只想接受音乐频道的某一位歌手发布的歌,而不是音乐频道的所有发布信息,为了实现这一功能,我们把上面代码改造一下:

var channel = {}, //Youtube上的一个音乐频道
obj = {};//按关键字key将订阅者分类

channel.on = function (key, fn) {//添加订阅这人数
    var stack, _ref;//stack为订阅key频道的所有人的消息, _ref临时变量
    //赋值操作成功后返回的不是true,而是赋的值,也就是变量里现在存的值,如果将一个未定义的变量赋值,返回undefined
    stack = (_ref = obj[key]) !== undefined  
    ? _ref 
    : obj[key] = []  
    stack.push(fn)
}

channel.emit = function () {//频道发布新的消息
    var key, stack, _ref, i, fn;
    key = [].shift.call(arguments)
    stack = (_ref = obj[key]) !== undefined 
    ? _ref 
    : obj[key] = []
    for (i=0;i<stack.length;i++) {
        fn = stack[i]
        fn.apply(this, arguments)
    }
}

//开始订阅
channel.on('Pg.lost', function (song) {//某人订阅了Tom Day这位歌手的动态
    console.log(`今天要发布新歌是:${song.author}-${song.name},类型:${song.type}`)
})

channel.emit('Tom Day',{//频道发布Tom Day歌手的一条消息
    author: 'Tom Day',
    type: 'Post-Rock',
    name: 'Flemington'
})

channel.emit('Pg.lost', {//频道发布Pg.lost歌手的一条消息
    author: 'Pg.lost',
    type: 'Post-Rock',
    name: 'Kardusen',
})
//今天要发布新歌是:Pg.lost-Kardusen,类型:Post-Rock

这样一来订阅者就可以接收到指定歌手的动态了

下面我们在为这个观察者模式增加取消订阅的功能:

channel.off = function (key) {//取消该歌手的所有订阅者
    var _ref;
    return (_ref = obj[key]) !== undefined 
    ? _ref.length = 0 
    : void 0;
}

我们来检验一下:

//开始订阅
channel.on('Pg.lost', function (song) {
    console.log(`今天要发布新歌是:${song.author}-${song.name},类型:${song.type}`)
})

//取消订阅
channel.off('Pg.lost')

//发布信息
channel.emit('Tom Day', {//频道发布Tom Day歌手的一条消息
    author: 'Tom Day',
    type: 'Post-Rock',
    name: 'Flemington'
})

取消订阅成功,但是这样会把所有订阅者都取消掉,下面我们再来改进一下,并且用ES6的语法重新组织一下上面的代码:

class Channel {
    constructor() {
        this._obj = {}
    }

    //订阅
    on(key, handler) {
        var events, _ref
        events = (_ref = this._obj[key]) !== undefined ? _ref : this._obj[key] = []
        events.push(handler)
    }

    //发布
    emmit() {
        var key, events, _ref
        key = [].shift.call(arguments)
        events = (_ref = this._obj[key]) ? _ref : this._obj[key] = []
            events.forEach(i => {
                i.apply(this, arguments)
            })
        }

    //解除订阅
    off(key, handler) {
        var events = this._obj[key]
        if(events)  this._obj[key] = events.filter(i => i !== handler)
    }

    //接收一次发布信息后取消订阅
    once(key, handler) {
        function temp () {
            handler.apply(this, arguments)
            this.off(key, temp)
        }
        this.on(key, temp)
    }

}

利用ES6的语法可以让我们的代码阅读起来更加清晰,我在这里还增加了一个once的方法,我们来测试一下

function fn1 (song) {
   console.log(`今天要发布新歌是:${song.author}-${song.name},类型:${song.type}`)
}

var musicTV = new Channel()

musicTV.once('Pg.lost', fn1)

musicTV.emmit('Pg.lost', {
    author: 'Pg.lost',
    type: 'Post-Rock',
    name: 'Kardusen',
})
musicTV.emmit('Pg.lost', {
    author: 'Pg.lost',
    type: 'Post-Rock',
    name: 'Yes I am',
})
//今天要发布新歌是:Pg.lost-Kardusen,类型:Post-Rock

其实浏览器的事件就用到了观察者模式:

window.addEventListener('scroll', fn, false)

这里fn函数(观察者)订阅了window对象的scroll事件, 当页面发生滚动时, 对应的fn函数就会执行

学习设计模式是为了更好的管理和设计代码、让代码更容易被他人理解

观察者模式的特点

1.观察者和被观察者是抽象耦合的。2.建立一套触发机制。

观察者模式的缺点

  1. 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
  2. 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
  3. 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

    适用场景

  4. 当一个抽象模型有两个方面, 其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
  5. 当对一个对象的改变需要同时改变其它对象, 而不知道具体有多少对象有待改变。
  6. 当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之, 你不希望这些对象是紧密耦合的。

参考资料

JS 设计模式 十三(观察者模式)
Javascript设计模式3】-观察者模式
JS设计模式笔记 – 观察者模式

mode_edit