设计模式5-发布-订阅模式(观察者模式)

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

定义

其定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变的时候,所有依赖它的对象都会得到通知。

主要解决

一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。

在JavaScript中,其主要有以下两个用处:

  • 可以广泛应用于异步编程之中,这是一种代替回调函数的方案。
  • 发布-订阅模式可以取代对象之间的硬编码通知机制,一个对象不用再显示地调用另外一个对象的某个接口。

实现

在web开发中,我们其实已经用到了发布-订阅模式,其就是DOM事件。

DOM事件

例如我们给一个按钮绑定一个点击事件:

1
2
3
4
5
function click(){
console.log('click')
}

document.getElmentById('button').addEventListener('click', click, false)

实际上这个事件绑定的过程也是一个发布订阅模式。我们预先将依赖添加到发布者,当发布者事件变化,即点击事件发生的时候,我们再触发该依赖。这就是一个发布订阅模式的触发过程。

简单实现

除了DOM事件,我们还会经常实现一些自定义的事件,这种依靠自定义事件完成的发布-订阅者模式可以用于任何JavaScript代码中。

首先我们要明确三个部分

  • 指定发布者。
  • 为发布者添加一个 缓存列表用于缓存回调函数,以用于通知订阅者。
  • 最后发布消息的时候,发布者遍历整个缓存列表,依次触发里面存放的回调函数。

下面来进行简单的开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let publisher = {}						//发布者

publisher.cacheList = [] //缓存列表,存放订阅者的回调函数

publisher.listen = function(fn){ //订阅消息
this.cacheList.push(fn) //将订阅者函数添加进缓存列表
}

publisher.trigger = function(){ //发布消息
for(let i = 0, len = this.cacheList.length; i < len; i++){
this.[cacheList].apply(this, arguments)
}
}

//简单测试
publisher.listen(function(time, msg){
console.log(time+'的消息为'+msg)
})

publisher.trigger(1, '你是猪')
publisher.trigger(2, '你是狗')

发布订阅者+关键字

在上面的例子中,虽然能够实现发布订阅这个功能,但是还有一个问题是不同的订阅者可能需要订阅对不同的消息,所以我们需要给消息加上一个key,以表示消息的类型。

实际上只是需要将消息队列定义为对象,以表示不同的key;在发布消息时,按照key来触发消息。

1
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

let publisher = {} //发布者

publisher.cacheList = {} //缓存列表,存放回调函数

publisher.listen = function(key, fn){
if(!this.cacheList[key]){ //如果还没有订阅过此类的消息,给该类消息创建一个缓存列表
this.cacheList[key] = []
}
this.cacheList[key].push(fn)
}

publisher.trigger = function(){
let key = Array.prototype.shift.call(arguments) //发布消息
let fns = this.cacheList[key] //取出对应的消息类型

if(!fns || fns.length === 0){ //如果没有订阅该消息,则返回
return false
}

for(let i = 0, len = fns.length; i < len; i++){
fns[i].apply(this, arguments) //arguments是发布消息时附送的参数
}
}


//测试
publiser.listen('消息类型1', function(msg){
console.log('消息类型1:'+msg)
})

publiser.listen('消息类型2', function(msg){
console.log('消息类型2:'+msg)
})

publisher.trigger('消息类型1', '你是猪')
//消息类型1:你是猪

publisher.trigger('消息类型2', '你是狗')
//消息类型2:你是狗

为任何对象添加发布订阅者

实际上发布者订阅者模式可以为任何对象添加,我们只需要将主要的三个属性添加到对象,即可使对象拥有发布订阅模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let installEvent = function(obj){
obj.cacheList = {}
obj.listen = function(key, fn){
if(!this.cacheList[key]){ //如果还没有订阅过此类的消息,给该类消息创建一个缓存列表
this.cacheList[key] = []
}
this.cacheList[key].push(fn)
}
obj.trigger = function(){
let key = Array.prototype.shift.call(arguments) //发布消息
let fns = this.cacheList[key] //取出对应的消息类型

if(!fns || fns.length === 0){ //如果没有订阅该消息,则返回
return false
}

for(let i = 0, len = fns.length; i < len; i++){
fns[i].apply(this, arguments) //arguments是发布消息时附送的参数
}
}
}

取消订阅的事件

有时候我们需要取消订阅的事件,现在我们来实现这个功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let remove = function(key, fn){
let fns = this.cacheList[key]
if(!fns){
return false
}
if(!fn){
fns && (fns.length = 0)
}else{
for(let i =fns.length; i > 0; i--){
let _fn = fns[i]
if(_fn === fn){
fns.splice(i, 1) //删除订阅者的回调函数
}
}
}
}

全局的发布-订阅对象

对于每一个需要实现发布订阅模式的对象,都需要在对象上添加相同的四个属性,虽然理论上没有性能损失不大,但是我们还是可以建立一个全局的发布订阅对象。

1
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
let Event = (function(){
let cacheList = {}
let listen, trigger, remove

listen = function(key, fn){
if(!cacheList[key]){
cacheList[key] = []
}
cacheList[key].push(fn)
}

trigger = function(){
let key = Array.prototype.shift.call(arguments)
let fns = cacheList[key]
if(!fns || fns.length === 0){
return false
}
for(let i = 0,fn; fn = fns[i++];){
fn.apply(this, arguments)
}
}

remove = function(key, fn){
let fns = cacheList[key]
if(!fns){
return false
}
if(!fn){
fns && (fns.length = 0)
}else{
for(let l = fns.length - 1; l >= 0; l--){
let _fn = fns[l]
if(_fn === fn){
fns.splice(l, i)
}
}
}
}

return {
listen,
trigger,
remove
}
})()

Event.listen('类型1', function(name){
console.log('类型1:'name)
})

Event.trigger('类型1', '垃圾')

//类型1:垃圾

全局事件的问题

全局事件虽然可以解决开销,但是却出现了其他问题:命名冲突

越来越多的发布订阅的添加,极其可能出现命名冲突的问题,由此我们如果使用全局事件,则必须使用命名空间来解决问题。

关于离线事件

在异步事件中,极可能出现我们添加监订阅不够及时,使得事件已经触发了。这样就会出现预期之外的错误了。所以在这种需求中,我们需要实现离线事件。此种情况,我们可以建立一个离线事件的堆栈,当发布的时候还没有对象来订阅此事件,则暂时将其存放至离线栈,等有对象来订阅此对象的时候,我们将遍历堆栈并且依次执行这些函数。

实现

下面我们将上面两点结合起来。

1
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
let Event = (function(){
let global = sthi,
Event,
_default = 'default'

Event = function(){
let _listen,
_trigger,
_remove,
_slice = Array.prototype.slice,
_shift = Array.prototype.shift,
_unshift = Array.prototype.unshift,
namespaceCache = {},
_create,
find,
each = function(ary, fn){
let ret
for(let i = 0, len = ary.length; i < len; i++){
let n = ary[i]
ret = fn.call(n, i, n)
}
return ret
}

_listen = function(key, fn, cache){
if(!cache[key]){
cache[key] = []
}
cache[key].push(fn)
}

_remove = function(key, fn, cache){
if(cache[key]){
if(fn){
for(let i = cache[key].length; i >= 0; i--){
if(cache[key][i] === fn){
cache[key].splice(i, 1)
}
}
}else{
cache[key] = []
}
}
}

_trigger = function(){
let cache = _shift.call(arguments),
key = _shift.call(arguments),
args = arguments,
_self = this,
stack = cache[key]
if(!stack || !stack.length){
return
}
return each(stack, function(){
return this.apply(_self, args)
})
}

_create = function(namespace){
let namespace = namespace || _default
let cache = {},
offlineStack = [],
ret = {
listen: function(key, fn, cache){
_listen(key, fn, cache)
if(offlineStack == null){
return
}
if(last === 'last'){
offlineStack.length && offlineStack.pop()
}else{
each(offlineStack, function(){
this()
})
}
offlienStack = null
},
one: function(key, fn, last){
_remove(key, cache)
this.listen(key, fn, last)
},
remove: function(key, fn){
_remove(key, cache, fn)
},
trigger: function(){
let fn,
args,
_self = this
_unshift.call(arguments, cache)
args = arguments
fn = function(){
return _trigger.apply(_self, args)
}
if(offlineStack){
return offlineStack.push(fn)
}
return fn()
}
}
return namespace ? (namespaceCache[namespace] ? namespaceCache[namespace] : namespaceCache[namespace] = ret) : ret
}
return {
create: _create,
one: function(key, fn, last){
let event = this.create()
event.one(key, fn, last)
},
remove: function(key, fn){
let event = this.create()
event.remove(key, fn)
},
list: function(key, fn, last){
let event = this.create()
event.listen(key, fn, last)
},
trigger: function(){
let event = this.create()
event.trigger.apply(this, arguments)
}
}
}
return Event
})()

JavaScript中的发布订阅模式

值得注意的是,之前我们编写的发布订阅模式,和一些其他的语言(比如Java)中的实现还是有区别的。在Java中实现一个自己的发布订阅模式,通常会把订阅者自身当作引用传入发布者对象中,同时订阅者对象还需提供一个名为诸如update的方法,供发布者对象在适合的时候调用。而在JavaScript中,我们用注册回调函数的形式来代替传统的发布订阅模式,更加方便。

参考

  • 《JavaScript设计模式与开发实践》

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2024 My Wonderland All Rights Reserved.

UV : | PV :