JavaScript设计模式-观察者模式

观察者模式(Observer Pattern),也被称为“发布/订阅模型(publisher/subscriber model)”。在这种模式中,有两类对象,分别是“观察者-Observer”和“目标对象-Subject”。目标对象中保存着一份观察者的列表,当目标对象的状态发生改变的时候就主动向观察者发出通知(调用观察者提供的方法),从而建立一种发布/订阅的关系。这一种发布/订阅的关系常用于实现事件、消息的处理系统。

在我们的生活中,也存在着许多观察者模式,最简单的例子就是“微博”。关注和被关注的关系,其实就是一个发布/订阅模型。假如,方舟子“悄悄关注”了天才韩寒,韩寒在微博上每发出一条消息都会反馈到方舟子的消息列表中,方舟子便可端坐家中,阴阴一笑,“嘿嘿,小子你干了什么我都知道……”,然后方舟子就开始行动了。

 

传统的观察者模式

我们先看看传统的观察者模式是怎样的吧(Java版):

//被观察者
public class Subject{
    private Array obArray = new Array();
    //增加一个观察者
    public void addObserver(Observer o){
        this.obArray.add(o);
    }
    //删除一个观察者
    public void removeObserver(Observer o){
        this.obArray.remove(o);
    }
    //通知所有观察者
    public void notifyObservers(){
        for(Observer o: this.obArray){
            o.update();
        }
    }
    public void doSomething(){
        //更新状态,告诉所有观察者
        this.notifyObservers();
    }
}
//观察者
public class Observer{
    public void update(){
        //目标对象更新了,要做点什么了
    }
}

 

使用观察者模式的好处是,当一个对象A需要通知另外一个对象B的时候,无需在A中声明B,在保持相关对象行为一贯性的时候,避免对象之间的紧耦合,这样可以使对象有很好的重用性。

 

DOM中的观察者模式

JavaScript是一个事件驱动型语言,观察者模式可谓随处可见,例如:

document.body.onclick = function(){
    alert('我是一个观察者,你一点击,我就知道了');
}
//或者是
document.body.addEventListener('click',function(){
    alert('我也是一个观察者,你一点击,我就知道了');
});

我们给body结点添加了一个onclick事件,一点击就会弹出对话框,这里面就存在着一个发布/订阅的模型。其中,body是发布者,即目标对象,当被点击的时候,向观察者反馈这一事件;JavaScript中函数也是一个对象,click这个事件的处理函数就是观察者,当接收到目标对象反馈来的信息时进行一定处理。

这个例子中的发布/订阅关系是由JavaScript语言本身实现的,DOM的每个节点都可以作为Subject,提供了很多事件处理(Event handle)的接口,你只需要给这些接口添加监听函数(也就是Observer),就可以捕获触发的事件进行处理。

 

JavaScript的观察者模式

然而在我们自己写的对象中,要实现这种发布/订阅的关系,就需要自己来实现这个观察者模型,例如:

var ObserverPattern= function(){
    //基于事件的观察者列表
    this.eventObsArray = {};
}
ObserverPattern.prototype = {
    //通知某个事件的所有观察者
    notifyObservers: function(eventName,datas){
        var observers= this.eventObsArray[eventName]||[],i,ob;
        for(i=0;ob=observers[i];i++){
            ob.handler.apply(ob.scope,datas||[]);
        }
    },
    //给某个事件添加观察者
    addObserver: function(eventName,handleFunction,observer){
        var events = this.eventObsArray,
        events[eventName] = events[eventName]||[];
        events[eventName].push({
            //传入的observer参数是handleFunction中的this
            scope: observer || this,
            handler: handleFunction
        });
    },
    //取消某个观察者对某事件的观察
    removeObserver: function(eventName,observer){
        var evts = this.eventObsArray;
        if(!evts[eventName]) return;
        evts[eventName]=evts[eventName].filter(function(ob){
            return ob.scope!=observer;
        });
    }
}
var 韩寒 = new ObserverPattern();
var 方舟子 = {
    doSomeResearch: function(){alert('嘿嘿…我在搞研究…')}
}
//韩寒一写博客,方舟子就开始研究了
韩寒.addObserver('写博客',function(){
    this.doSomeResearch();
},方舟子);

看过了传统的观察者模式,再看JavaScript版的,或许你会好奇,怎么只有Subject,那个Observer类去哪了?因为在JavaScript中,函数也是一等公民,是对象,无需依赖于其他对象而存在,因此无需专门写一个Observer的构造函数,所有对象都可以做Observer(是不是觉得比传统的灵活多了)。

然而这种形式的发布/订阅模型,还是有些不足的地方,整个关系链条是由目标对象维护的,观察者无法主动去监听目标对象的变化;其次,观察者不知道其他观察者的存在,有时一个观察者的处理有时还会触发其他的事件,无法让其他观察者进行后续处理。

 

既是目标对象也是观察者

方舟子观察韩寒,难道韩寒就不可以看看方舟子了?其实,目标对象也可以是观察者,咱们对上面的ObserverPattern再改进改进:

var ObserverPattern= function(obj){
    for(var i in obj){
        this[i] = obj[i];
    }
    this.eventObsArray = {};
}
ObserverPattern.prototype = {
    //监听某个目标对象
    listen: function(subject, eName, handler){
        subject.addObserver(eName, handler, this)
    },
    //取消监听某个目标对象
    ignore: function(subject, eName){
        subject.removeObserver(eName,this);
    },
    //之前定义的方法,这里就不多说了
    notifyObservers: function(eName,datas){},
    addObserver: function(eName,handler,ob){},
    removeObserver: function(eName,ob){}
}

var 韩寒 = new ObserverPattern({
    postReward: function(){alert('研究吧, 奖金2000万…')},
    writeBlog: function(){this.notifyObservers('写博客')}
});
var 方舟子 = new ObserverPattern({
    doSomeResearch: function(){
        alert('嘿嘿…我在搞研究…');
        this.notifyObservers('搞研究')
    }
});

//韩寒一发微博,方舟子就开始研究了
方舟子.listen(韩寒,'写博客',方舟子.doSomeResearch);

//方舟子一开始研究,韩寒就发赏金了
韩寒.listen(方舟子,'搞研究',韩寒.postReward);

一个事件可能会产生多方面的影响,而事件消息的发出者不一定能知道所有被影响的对象。将目标对象和观察者整合起来之后,观察者就可以主动监听目标对象,无需目标对象来维护整个关系链条;从开发的角度来说,模块的划分更加明确,无需关注外部模块的实现,只需要监听它们发出的事件即可。

同时,当把目标对象和观察者整合到一起的时候,就形成了一条事件的触发链,一个事件可以触发另一个事件,一个观察者可以将自己观察的结果告诉其他观察者。当然,也要小心事件的循环促发,或者像”蝴蝶效应”那样让一个无关紧要的事件产生过大的影响。

 

更加灵活的事件管理方式

上面的ObserverPattern已经相对完善了,但是使用起来还是有不少限制。例如,需要保证目标对象和观察者先被创建才被调用;一个事件只能被一个目标对象触发,无法一个事件监听多个消息来源。虽然这些也不算什么大问题,但是还有一种更加灵活的方式来管理我们的事件。

//全局的事件监听模块,可用于对象之间的消息传递
var Event = (function(){
    var events = {},
    registerEvent = function(eName, handler, scope){
        events[eName] = events[eName] || [];
        events[eName].push({
            scope: scope || this,
            handler: handler
        });
    },
    removeEvent = function(eName, handler, scope){
        scope = scope || this;
        if(!fns) return;
        events[eName] = events[eName].filter(function(fn){
            return fn.scope!=scope || fn.handler!=handler
        });
    },
    triggerEvent = function(eventName,params){
        var fns = events[eventName],i,fn;
        if(!fns) return;
        for(i=0;fn=fns[i];i++){
            fn.handler.apply(fns.scope,params||[]);
        }
    };
    return {
        listen: registerEvent,
        ignore: removeEvent,
        trigger: triggerEvent
    }
})();

Event.listen('韩寒写博客', 方舟子.doSomeResearch, 方舟子);
(function(){
    alert('我是路人甲,我告诉方舟子,韩寒写博客了');
    Event.trigger('韩寒写博客');
})();

到了这一步,目标对象已经完全的被淡化,是谁发布的信息已经无关紧要了,开发时只需关注观察者对事件的处理方式。方舟子的研究已经不依赖于韩寒的存在,只需要有网友不断的给他提供小道消息,方舟子就可以挖掘出越来越多有趣的东西来了[大笑] ~

这种方式的确更为灵活,但越是灵活就越是不好把握,这是一把双刃剑,要小心使用。这种情况下,观察者与目标对象之间的依存关系是很难被跟踪的,很容易像“蝴蝶效应”那样产生意想不到的结果。

 

最后说一下,韩粉方粉别太在意,我不是故意拿你们教主来开刷的,只是碰巧这样很形象嘛 [嘻嘻] ~

 

发布者

Rolf

伪文艺IT攻城师,热爱前端,热爱互联。

《JavaScript设计模式-观察者模式》有3个想法

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

[喜欢] [嘻嘻] [奋斗] [问号] [鼓掌] [泪] [酷] [强] [耶] [握手] [心] [给力] [神马] [围观] [奥特曼] more »