第八章-发布订阅模式
定义:发布-订阅模式又称为观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都将得到通知。
# 现实中的发布-订阅模式
小明买房子,略......
# DOM 事件
如果我们曾经在 DOM 节点上绑定过事件函数,那么我们就一定使用过发布-订阅模式,来看看下面这两句简单的代码发生了什么事情:
document.body.addEventListener("click", function(){
    console.log(2);
});
document.body.click(); // 模拟用户点击
2
3
4
在这里需要监控用户点击 document.body 的动作,但是我们没办法知道用户会在什么时候点击。所以我们订阅了 document.body 上的 click 事件,当 body 节点被点击时,body 节点便会向订阅者发布这个消息。
# 自定义事件
我们还是用小明买房的背景故事,来模拟当出现满足小明要求的房源后,中介会通知小明,示例代码如下:
var salesOffices = {}; // 售楼处
salesOffices.clientList = [];
salesOffices.listen = function (fn) {
    this.clientList.push(fn);
};
salesOffices.trigger = function () {
    for (var i = 0, fn; fn = this.clientList[i]; i++) {
        fns.apply(this, arguments);
    }
};
salesOffices.listen(function (squareMeter, price) {
    console.log('面积' + squareMeter + '价格:' + price);
});
salesOffices.listen(function (squareMeter, price) {
    console.log('面积' + squareMeter + '价格:' + price);
});
salesOffices.trigger(88, 2000000);
salesOffices.trigger(110, 5000000);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
上面已经实现了一个最简单的发布-订阅模式,但是这里面还存在一些问题。我们看到订阅者接收到了发布者发布的每个消息,小明只想买 88 平米的房子,发布者却把 100 平米的房子的信息也推送给了小明。所以我们有必要增加一个标识 key,让订阅者只订阅自己感兴趣的消息,改写后的代码如下:
var salesOffices = {}; // 售楼处
salesOffices.clientList = {};
salesOffices.listen = function (key, fn) {
    if (!this.clientList[key]) {
        this.clientList[key] = [];
    }
    this.clientList[key].push(fn);
};
salesOffices.trigger = function () {
    var key = Array.prototype.shift.call(arguments);
    fns = this.clientList[key];
    if (!fns || fns.length === 0) {
        return false;
    }
    for (var i = 0; i < fns.length; i++) {
        fns[i].apply(this, arguments);
    }
};
salesOffices.listen('squareMeter88', function (price) {
    console.log('价格:' + price);
});
salesOffices.listen('squareMeter110', function (price) {
    console.log('价格:' + price);
});
salesOffices.trigger('squareMeter88', 2000000);
salesOffices.trigger('squareMeter110', 5000000);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 发布-订阅模式的通用实现
现在我们已经看到了如何让售楼处拥有接受订阅和发布事件的功能。如果小明又去另一个售楼处看房,那么这段代码又要重写一次,那么能不能做一个通用的发布-订阅功能呢?显然是可以的,我们尝试编写如下代码:
var event = {
    clientList: [],
    listen: function (key, fn) {
        if (!this.clientList[key]) {
            this.clientList[key] = [];
        }
        this.clientList[key].push(fn);
    },
    trigger: function () {
        var key = Array.prototype.shift.call(arguments);
        fns = this.clientList[key];
        if (!fns || fns.length === 0) {
            return false;
        }
        for (var i = 0, fn; (fn = fns[i++]); ) {
            fn.apply(this, arguments);
        }
    }
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
然后再定义一个 installEvent 函数,这个函数可以给所有的对象都动态安装发布-订阅功能:
var installEvent = function (obj) {
    for (var i in event) {
        obj[i] = event[i];
    }
};
2
3
4
5
然后我们来测试一下,我们给售楼处对象动态的增加发布-订阅功能:
var salesOffices = {};
installEvent(salesOffices);
salesOffices.listen(
    'squareMeter88',
    (fn1 = function (price) {
        console.log('价格:' + price);
    })
);
salesOffices.listen('squareMeter88', function (price) {
    console.log('价格:' + price);
});
salesOffices.listen('squareMeter110', function (price) {
    console.log('价格:' + price);
});
salesOffices.trigger('squareMeter88', 2000000);
salesOffices.trigger('squareMeter110', 5000000);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 取消订阅的事件
有时候,我们也许需要取消已经订阅的事件的功能。比如小明突然不想买房子了,为了避免继续接到售楼的信息,小明需要将之前订阅的事件取消。现在我们给 event 对象增加 remove 方法:
remove: function (key, fn) {
    var fns = this.clientList[key];
    if (!fns) {
        return false;
    }
    if (!fn) {
        fns && (fns.length = 0);
    } else {
        for (var i = fns.length - 1; i >= 0; i--) {
            var _fn = fns[i];
            if (_fn === fn) {
                fns.splice(i, 1);
            }
        }
    }
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
然后再来测试一番
salesOffices.listen(
    'squareMeter88',
    (fn1 = function (price) {
        console.log('价格:' + price);
    })
);
salesOffices.listen('squareMeter88', function (price) {
    console.log('价格:' + price);
});
salesOffices.listen('squareMeter110', function (price) {
    console.log('价格:' + price);
});
salesOffices.remove('squareMeter88', fn1);
salesOffices.trigger('squareMeter88', 2000000);
salesOffices.trigger('squareMeter110', 5000000)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 全局的发布-订阅对象
var Event = (function () {
    var clientList = {},
        listen,
        trigger,
        remove;
    listen = function (key, fn) {
        if (!clientList[key]) {
            clientList[key] = [];
        }
        clientList[key].push(fn);
    };
    trigger = function () {
        var key = Array.prototype.shift.call(arguments);
        fns = clientList[key];
        if (!fns || fns.length === 0) {
            return false;
        }
        for (var i = 0, fn; (fn = fns[i++]); ) {
            fn.apply(this, arguments);
        }
    };
    remove = function (key, fn) {
        var fns = clientList[key];
        if (!fns) {
            return false;
        }
        if (!fn) {
            fns && (fns.length = 0);
        } else {
            for (var i = fns.length - 1; i >= 0; i--) {
                var _fn = fns[i];
                if (_fn === fn) {
                    fns.splice(i, 1);
                }
            }
        }
    };
    return {
        listen,
        trigger,
        remove,
    };
})();
Event.listen('squareMeter88', function (price) {
    console.log('价格:' + price);
});
Event.trigger('squareMeter88', 2000000);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
这样的话,调用者就不需要知道发布者是谁了,直接订阅对应的事件就可以了。
发布-订阅模式的问题,如果模块与模块之间用到了发布-订阅模式,那就是将一些逻辑隐藏在了背后,我们可能会搞不清楚这个消息是从哪个模块发送过来的,也不知道会有多少人订阅了这个消息,这样无疑会增加我们的维护成本,所以在实际的开发中,要结合真实的场景慎用发布-订阅模式。
