JS实现观察者模式

来源:岁月联盟 编辑:exp 时间:2012-10-25

基本概念介绍

观察者(observer) 模式广泛用于客户端Javascript编程中。所有的浏览器事件都是该模式的例子。它的另一个名字也称为自定义事件(custom events),与那些由浏览器触发的事件相比,自定义事件表示是由你编程实现的事件。此外,该模式的另一个别名也称为订阅/发布(subscriber/publisher)模式。

设计该模式背后的主要动力是促进形成松散耦合。在这种模式中,并不是一个对象调用另一个对象的方法,而是一个对象订阅另一个对象的特定活动并在状态改变后获得通知。订阅者也称为观察者,而补观察的对象称为发布者或主题。当发生了一个重要的事件时,发布者将会通知(调用)所有订阅者并且可能经常以事件对象的形式传递消息。

示例:杂志订阅

假设有一个发布者paper,它每天出版报纸及月刊杂志。订阅者joe将被通知任何时候所发生的新闻。

该paper对象需要一个subscribers属性,该属性是一个存储所有订阅者的数组。订阅行为只是将其加入到这个数组中。当一个事件发生时,paper将会循环遍历订阅者列表并通知它们。通知意味着调用订阅者对象的某个方法。故当用户订阅信息时,该订阅者需要向paper的subscribe()提供它的其中一个方法。

paper也提供了unsubscribe()方法,该方法表示从订阅者数组(即subscribers属性)中删除订阅者。paper最后一个重要的方法是publish(),它会调用这些订阅者的方法,总而言之,发布者对象paper需要具有以下这些成员:

1.subscribers 一个数组 2.subscribe() 将订阅者添加到subscribers数组中 3.unsubscribe() 从subscribers数组中删除订阅者 4.publish() 循环遍历subscribers数组中的每一个元素,并且调用他们注册时所提供的方法

所有这三种方法都需要一个type参数,因为发布者可能触发多个事件(比如同时发布一本杂志和一份报纸)而用户可能仅选择订阅其中一种,而不是另外一种。

由于这些成员对于任何发布者对象都是通用的,故将它们作为独立对象的一个部分来实现是很有意义的。那样我们可将其复制到任何对象中,并将任意给定对象变成一个发布者。

如下实现一个通用发布者

var publisher = {
    subscribers: {
        any: [] // event type: subscribers
    },
    subscribe: function (fn, type) {
        type = type || 'any';
        if (typeof this.subscribers[type] === "undefined") {
            this.subscribers[type] = [];
        }
        this.subscribers[type].push(fn);
    },
    unsubscribe: function (fn, type) {
        this.visitSubscribers('unsubscribe', fn, type);
    },
    publish: function (publication, type) {
        this.visitSubscribers('publish', publication, type);
    },
    visitSubscribers: function (action, arg, type) {
        var pubtype = type || 'any',
            subscribers = this.subscribers[pubtype],
            i,
            max = subscribers.length;
 
        for (i = 0; i < max; i += 1) {
            if (action === 'publish') {
                subscribers[i](arg);
            } else {
                if (subscribers[i] === arg) {
                    subscribers.splice(i, 1);
                }
            }
        }
    }
};
而这里有一个函数makePublisher(),它接受一个对象作为对象,通过把上述通用发布者的方法复制到该对象中,从而将其转换为一个发布者:

function makePublisher(o) {
    var i;
    for (i in publisher) {
        if (publisher.hasOwnProperty(i) && typeof publisher[i] === "function") {
            o[i] = publisher[i];
        }
    }
    o.subscribers = {any: []};
}
现在,让我们来实现paper对象,它所做的就是发布日报和月刊:

var paper = {
    daily: function () {
        this.publish("big news today");
    },
    monthly: function () {
        this.publish("interesting analysis", "monthly");
    }
};
将paper构造成一个发布者:

makepublisher(paper);
由于已经有一个发布者,让我们来看看订阅者对象joe,该对象有两个方法:

var joe = {
    drinkCoffee: function (paper) {
        console.log('Just read ' + paper);
    },
    sundayPreNap: function (monthly) {
        console.log('About to fall asleep reading this ' + monthly);
    }
};
现在,paper注册joe(即joe向paper订阅):

paper.subscribe(joe.drinkCoffee);
paper.subscribe(joe.sundayPreNap, 'monthly');
即joe为默认“any”事件提供了一个可被调用的方法,而另一个可被调用的方法则用于当“monthly”类型的事件发生时的情况。现在让我们来触发一些事件:

paper.daily();
paper.daily();
paper.daily();
paper.monthly();
paper.monthly();
paper.monthly();
输出为:

Just read big news today
Just read big news today
Just read big news today
About to fall asleep reading this interesting analysis
About to fall asleep reading this interesting analysis
About to fall asleep reading this interesting analysis
【进一步扩展】让joe成为发布者(使用微博和博客时任何人者可以是发布者)。

makePublisher(joe);
joe.tweet = function (msg) {
    this.publish(msg);
};
而paper的公关部门决定读取读者的tweet,并且订阅joe的信息,那么需要提供方法readTweets():

paper.readTweets = function (tweet) {
    alert('Call big meeting! Someone ' + tweet);
};
joe.subscribe(paper.readTweets);
此时,只要joe发出tweet消息,paper都会得到提醒:

joe.tweet("hated the paper today");
完整性代码如下:

<!doctype html>
<html>
<head>
    <title>Observer</title>
</head>   
<body>
 
 
<script>
"use strict";
var publisher = {
    subscribers: {
        any: [] // event type: subscribers
    },
    subscribe: function (fn, type) {
        type = type || 'any';
        if (typeof this.subscribers[type] === "undefined") {
            this.subscribers[type] = [];
        }
        this.subscribers[type].push(fn);
    },
    unsubscribe: function (fn, type) {
        this.visitSubscribers('unsubscribe', fn, type);
    },
    publish: function (publication, type) {
        this.visitSubscribers('publish', publication, type);
    },
    visitSubscribers: function (action, arg, type) {
        var pubtype = type || 'any',
            subscribers = this.subscribers[pubtype],
            i,
            max = subscribers.length;
 
        for (i = 0; i < max; i += 1) {
            if (action === 'publish') {
                subscribers[i](arg);
            } else {
                if (subscribers[i] === arg) {
                    subscribers.splice(i, 1);
                }
            }
        }
    }
};
 
/*
var s1 = {log: console.log},
    s2 = {err: console.error},
    s3 = {warn: console.warn};
 
 
publisher.subscribe(s1.log);
publisher.subscribe(s2.err);
publisher.subscribe(s3.warn);
 
publisher.publish({hello: "World"});
 
publisher.unsubscribe(s2.err);
publisher.publish("hello");
 
 
publisher.subscribe(s1.log, "log");
publisher.publish({obj: "log this object"}, "log");
*/
 
function makePublisher(o) {
    var i;
    for (i in publisher) {
        if (publisher.hasOwnProperty(i) && typeof publisher[i] === "function") {
            o[i] = publisher[i];
        }
    }
    o.subscribers = {any: []};
}
 
var paper = {
    daily: function () {
        this.publish("big news today");
    },
    monthly: function () {
        this.publish("interesting analysis", "monthly");
    }
};
 
makePublisher(paper);
 
var joe = {
    drinkCoffee: function (paper) {
        console.log('Just read ' + paper);
    },
    sundayPreNap: function (monthly) {
        console.log('About to fall asleep reading this ' + monthly);
    }
};
 
paper.subscribe(joe.drinkCoffee);
paper.subscribe(joe.sundayPreNap, 'monthly');
 
paper.daily();
paper.daily();
paper.daily();
paper.monthly();
 
 
makePublisher(joe);
 
joe.tweet = function (msg) {
    this.publish(msg);
};
 
paper.readTweets = function (tweet) {
    alert('Call big meeting! Someone ' + tweet);
};
 
joe.subscribe(paper.readTweets);
 
joe.tweet("hated the paper today");
</script>
 
</body></html>