我看网上有些人说观察者模式和发布者/订阅者模式不好区分容易混淆,说是人们常常混为一谈,但是看《js设计模式》的时候,觉得二者的结构差异并不是很模糊的。
《设计模式》里面的观察者模式是:创建主体和观察者两个对象的功能(两个功能对象),然后把这两个对象的方法属性给予具体的实例如DOM或js对象,让后让具体实例来完成“主体状态改变--观察者执行响应”。
主要是主体有一个属性,是一个数组(比如叫observerList),用来存储关注她的观察者;以及一个notify()方法,每次状态改变都从observerList里面遍历一次,依次触发观察者的update方法。
他给出的观察者模式只有主体和观察者两个东西。
试写了一个简单的观察者模式的小例子:
一个作为输入的input(即主体),一个add按钮给.container这个div添加input,并将添加的input作为观察者加入主体的观察者列表。
当主体输入数字时,自动触发input事件,下边的input自动显示双倍数字。
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Observers</title> 6 </head> 7 <body> 8 <input type="text" id="subect"> 9 <button id="addBut">add</button> 10 <div id="container"> 11 12 </div> 13 14 <script> 15 //list 16 function Olist(arr){ 17 this.olist=arr||[]; 18 } 19 //list.add 20 Olist.prototype.add=function(obj){ 21 this.olist.push(obj); 22 } 23 //list.length 24 Olist.prototype.count=function(){ 25 return this.olist.length; 26 } 27 //list.get 28 Olist.prototype.get=function(i){ 29 return this.olist[i]; 30 } 31 //Subject 32 function Subject(){ 33 this.observers=new Olist(); 34 } 35 //Subject.add 36 Subject.prototype.addo=function(obj){ 37 this.observers.add(obj) 38 } 39 //Subject.notify 40 Subject.prototype.notify=function(signal){ 41 var l = this.observers.count(); 42 for(var i=0;i<l;i++){ 43 this.observers.get(i).update(signal); 44 } 45 } 46 //observer 47 function Observer(){ 48 49 } 50 //observer.update 51 Observer.prototype.update=function(signal){ 52 this.value=Number(signal)*2; 53 } 54 55 //extend 56 function extend(obj,extension){ 57 for(var key in obj){ 58 extension[key]=obj[key] 59 } 60 } 61 //getDOM 62 var s = document.getElementById("subect"); 63 var a = document.getElementById("addBut"); 64 var c = document.getElementById("container"); 65 //Subject--s 66 extend(new Subject(),s); 67 //add observer 68 a.onclick=function(){ 69 var son=document.createElement("input") 70 son.type="text"; 71 son.value=""; 72 extend(new Observer,son); 73 s.addo(son); 74 c.appendChild(son); 75 76 } 77 //notify input-event 78 s.oninput=function(){ 79 this.notify(this.value); 80 } 81 82 </script> 83 </body> 84 </html>
但是发布/订阅模式的实现是在两者中间多了一个pubsub对象,这个pubsub对象有publish()、subscribe()、unsubscribe()方法,客体先pubsub.subscribe()某个事件,当主体发布了某个事件时要调用pubsub.publish("topic"),然后在pubsub对象的闭包中遍历已存储的事件,如果已存储的事件中有"topic",就在订阅了topic的对象列表中依次触发它们的回调。
从实现上看,pub/sub模式更像是对前者的改进,因为它实现了主体和观察者的解耦。
这意味着,pubsub在代码结构中是一个独立的存在,内部存储着 被订阅的主题类型(英文是topic,但意义和事件类似) 及 该主题发布时的回调 ,只是对外暴露了三个核心方法
subscribe():作用是往pubsub中插入订阅的主题并创建该主题下的回调函数数组
publish():发布某个主题,搜寻是否有关于该主题的订阅,有的话就遍历执行该主题对应的回调函数列表
unsubscribe():取消对某个主题的订阅(将回调函数从该主题的回调函数列表中剔除)
在《js设计模式》示例9-2和9-4中,给的都是一个ui更新的例子。即数据更新后,发布一个“dataAvailable”主题并把更新的数据作为参数传入.publish()中。而subscribe(“dataAvailable”,callback)的回调中则往页面上添加html向用户显示数据。
最后一个示例针对ajax应用,一开始看觉得这代码的顺序有点乱,然后上网找到Addy Osmani在2011年写的文章:https://msdn.microsoft.com/en-us/magazine/hh201955.aspx
文章最后的示例是同一个,但排版结构比中译版更清晰......
实际上,我们可以认为.subscribe()的回调是对.publish()的响应,而在一个subscribe()回调中,我们也能publish()另一个主题,触发另外的操作任务。(实现了时间上的顺序执行和空间上的代码分离,但是这本质上就跟函数调用无二)
另外一点,对于同一个topic,可以有多个subscribe()回调, 如示例中对于“search/tags”这个主题,安插了两个任务,一个发起请求,一个在页面上显示提示信息。所以在等待请求返回期间ui上显示搜索中,请求返回后发布另一个“search/result”主题展示结果。
写了个简单的pubsub实现:
<input type="button" value="click" id="cc">
1 var p={}; 2 (function(p){ 3 var topics={},uid=0; 4 var publish=function(topic,data){ 5 if(!topics[topic]){ 6 return false; 7 } 8 var subers=topics[topic]; 9 var l=subers.length; 10 for(var i=0;i<l;i++){ 11 subers[i].callback(topic,data); 12 } 13 } 14 15 p.publish=function(topic,data){ 16 publish(topic,data); 17 } 18 p.subscribe=function(topic,func){ 19 if(!topics[topic]){ 20 topics[topic]=[]; 21 } 22 var token=(++uid).toString(); 23 topics[topic].push({token:token,callback:func}); 24 return token; 25 } 26 }(p)) 27 28 var cc=document.getElementById("cc"); 29 //subscribe note1 30 var event_1=p.subscribe("note1",function(topic,data){ 31 console.log(data + " has been clicked!") 32 //call note2 33 p.publish("note2"); 34 }) 35 //subscribe note2 36 var event_2=p.subscribe("note2",function(topic,data){ 37 console.log("this is note2") 38 39 }) 40 cc.onclick=function(e){ 41 var e = e||window.event; 42 var target = e.target || e.srcElement; 43 p.publish("note1",target.id); 44 }