javascript设计模式-观察者模式
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>观察者模式</title> 5 <meta charset="utf-8"> 6 </head> 7 <body> 8 <script> 9 function extend(subclass, superclass) { 10 var F = function () { 11 }; 12 F.prototype = superclass.prototype; 13 subclass.prototype = new F(); 14 subclass.prototype.constructor = subclass; 15 subclass.super = superclass.prototype; 16 17 18 if (superclass.prototype.constructor === Object.prototype.constructor) { 19 superclass.prototype.constructor = superclass; 20 } 21 } 22 </script> 23 <script> 24 /** 25 * 观察者模式 26 * 27 * 定义: 28 * 定义对象间的一种一对多的依赖关系。当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。 29 * 30 * 本质: 31 * 触发联动 32 * 33 * 本质: 34 * 当修改目标对象的状态的时候,就会触发相应的通知,然后会循环调用所有注册的观察者对象的相应方法,其实就相当于联动调用这些观察者的方法。 35 * 而且这个联动还是动态的,可以通过注册和取消注册来控制观察者,因而可以在程序运行期间,通过动态地控制观察者,来变相地实现添加和删除某些功能处理,这些功能就是观察者在update的时候执行的功能。 36 * 同时目标对象和观察者对象的解耦,又保证了无论观察者发生怎样的变化,目标对象总是能够正确地联动起来。 37 * 38 * 39 * 在事件驱动的环境中。比如浏览器这种持续寻求用户关注的环境中,观察者模式(又名发布者-订阅者(publisher-subscriber)模式)是一种管理人与其人物之间的关系(确却的讲,是对象及其行为和状态之间的关系)的得力工具。这种模式的实质就是你可以对程序中某个对象的状态进行观察,并且在其发生改变时能够得到通知。 40 * 观察者模式中存在两个角色:观察者和被观察者,又叫发布者和订阅者。 41 * 42 * 认识观察者 43 * 44 * 1.目标和观察者之间的关系 45 * 按照模式的定义,目标和观察者之间是典型的一对多的关系。 46 * 但是要注意,如果观察者只有一个,也是可以的,这样就变相实现了目标和观察者之间一对一的关系,这也使得在处理一个对象的状态变化会影响到另一个对象的时候,也可以考虑使用观察者模式。 47 * 同样地,一个观察者也可以观察多个目标,如果观察者为多个目标定义的通知更新方法都是update方法的话,这会带来麻烦,因为需要接受多个目标的通知,如果一个update方法,那就需要在方法内部区分,到底这个更新的通知来自于哪个目标,不同的目标有不同的后续操作。 48 * 一般情况下,观察者应该为不同的观察者目标定义不同的回调方法,这样实现最简单,不需要在update方法内部进行区分。 49 * 50 * 2.单向依赖 51 * 在观察者模式中,观察者和目标是单向依赖的,只有观察者依赖于目标,而目标是不会依赖于观察者的。 52 * 它们之间联系的主动权掌握在目标手中,只有目标知道什么时候需要通知观察者,在整个过程中,观察者始终是被动的,被动地等待目标的通知,等待目标传值给它。 53 * 对目标而言,所有的观察者都是一样的,比如某些状态变化,只需要通知部分观察者,但那是属于稍微变形的用法了,不属于标准的,原始的观察者模式了。 54 * 55 * 3.基本的实现说明 56 * 1)具体的目标实现对象要能维护观察者的注册信息,最简单的实现方案就如同下面的例子,采用一个集合来保存观察者的注册信息。 57 * 2)具体的目标实现对象需要维护引起通知的状态,一般情况下是目标自身的状态,变形使用情况下,也可以识别的对象的状态。 58 * 3)具体的观察者实现对象需要能接受目标的通知,能够接受目标传递的数据,或者是能够主动去获取目标的数据,并进行后续处理。 59 * 4)如果是一个观察者观察多个目标,那么在观察者的更新方法里面,需要去判断是来自哪一个目标。一种简单的解决方案就是扩展update方法,比如在方法里面多传递一个参数进行区分等;还有一种更简单的方法,那就是干脆定义不同的回调方法。 60 * 61 * 4.命名建议 62 * 1)观察者模式有被称为发布订阅模式 63 * 2)目标接口的定义,建议在名称后面跟Subject 64 * 3)观察者接口的定义,建议在后面跟Observer 65 * 4)观察者接口的更新方法,建议名称为uodate,当然方法的参数可以根据需要定义,参数个数不限,参数类型不限。 66 * 67 * 5.触发时机 68 * 在实现观察者模式的时候,一定要注意触发通知的时机。一般情况下,是在完成了状态维护后触发,因为通知会传递数据,不能够先通知后改数据,这很容易出问题,会导致观察者和目标对象的状态不一致。 69 * 70 * 6.相互观察 71 * 在某些应用中,可能会出现目标和观察者相互观察的情况。比如有两套观察者模式的应用,其中一套观察者模式的实现是A对象,B对象观察C对象;在另一套观察者模式的实现里面,实现的是B对象,C对象观察A对象,那么A对象和C对象就是在相互观察, 72 * A对象的状态便会引起C对象的联动操作,反过来,C对象的状态变化也会引起A对象的联动操作。对于出现这种状况,要特别小心处理,因为可能会出现死循环的情况。 73 * 74 * 7.观察者模式的调用顺序 75 * 一,准备阶段(维护目标和观察者关系的阶段) 76 * 1.创建目标对象 77 * 2.创建观察者 78 * 3.向目标对象注册观察者对象 79 * 80 * 二,运行阶段 81 * 1.改变目标对象的状态 82 * 2.通知所有注册的观察者对象进行相应的处理 83 * 3.回调目标对象,获取相应的数据 84 * 85 * 8.通知的顺序 86 * 从理论上来说,当目标对象的状态变化后通知所有观察者的时候,顺序是不确定的,因此观察者实现的功能,绝对不要依赖于通知的顺序。也就是说,多个观察者之间的功能是平行的,相互不应该有先后的依赖关系。 87 * 88 * 89 * 何时使用 90 * 1.当一个抽象模型有两个方面,其中一个方面的操作依赖于另一个方面的状态变化,那么就可以选用观察者模式,将这两者封装成观察者和目标对象,当目标对象变化的时候,依赖于它的观察者对象也会发生相应的变化。这样就把抽象模型的这两个方面分离开了,使得他们可以独立地改变和复用。 91 * 2.如果在更改一个对象的时候,需要同时连带改变其他的对象,而且不知道究竟应该有多少对象需要被连带改变,这种情况可以选用观察者模式,被更改的哪一个对象很明显就相当于是目标对象,而需要连带修改的多个其他对象,就作为多个观察者对象了。 92 * 3.当一个对象必须通知其他对象,但是你又希望这个对象和其他被它统治的对象是松散耦合的。这个对象相当于是目标对象,而被它通知的对象就是观察者对象了。 93 */ 94 95 (function () { 96 // 示例代码 97 98 /** 99 * 目标对象,它知道观察它的观察者,并提供注册和删除观察者的接口 100 */ 101 var Subject = function () { 102 // 用来保存注册的观察者对象 103 this.observers = []; 104 }; 105 Subject.prototype = { 106 // 注册观察者对象 107 attach: function (observer) { 108 this.observers.push(observer); 109 }, 110 // 删除观察者对象 111 detach: function (observer) { 112 for (var i = 0, len = this.observers.length; i < len; i++) { 113 if (observer = this.observers[i]) { 114 this.observers.splice(i, 1); 115 return true; 116 } 117 } 118 119 return false; 120 }, 121 // 通知所有注册的观察者对象 122 notifyObservers: function () { 123 for (var i = 0, len = this.observers.length; i < len; i++) { 124 this.observers[i].update(this); 125 } 126 } 127 }; 128 129 /** 130 * 具体的目标对象,份额则把有关状态存入到相应的观察者对象 131 * 并在自己状态发生改变时,通知各个观察者 132 */ 133 var ConcreteSubject = function () { 134 ConcreteSubject.super.constructor.call(this); 135 // 目标对象状态 136 this.subjectState = ''; 137 }; 138 extend(ConcreteSubject, Subject); 139 ConcreteSubject.prototype.getSubjectState = function () { 140 return this.subjectState; 141 }; 142 ConcreteSubject.prototype.setSubjectState = function (subjectState) { 143 this.subjectState = subjectState; 144 // 状态发生改变,通知各个观察者 145 this.notifyObservers(); 146 }; 147 148 // 观察者接口,定义一个更新的接口给那些在目标发生改变的时候被通知的对象 149 var Observer = function () { 150 }; 151 Observer.prototype.update = function () { 152 }; 153 154 var ConcreteObserver = function () { 155 this.observerState = ''; 156 }; 157 extend(ConcreteObserver, Observer); 158 ConcreteObserver.prototype.update = function (subject) { 159 this.observerState = subject.getSubjectState(); 160 }; 161 }()); 162 163 (function () { 164 // 订阅报纸,拉模型 165 166 // 目标对象,作为被观察者,报社 167 function Subject() { 168 this.readers = []; 169 } 170 171 Subject.prototype = { 172 // 报纸的读者需要向报社订阅,先要注册 173 attach: function (reader) { 174 this.readers.push(reader); 175 }, 176 detach: function (reader) { 177 for (var i = 0, len = this.readers.length; i < len; i++) { 178 if (reader === this.readers[i]) { 179 this.readers.splice(i, 1); 180 return; 181 } 182 } 183 }, 184 // 当每期报纸印刷出来后,就要迅速主动地被送到读者的手中 185 // 相当于通知读者,让他们知道 186 notifyObservers: function () { 187 for (var i = 0, len = this.readers.length; i < len; i++) { 188 this.readers[i].update(this); 189 } 190 } 191 }; 192 193 // 报纸对象,具体的目标实现 194 function NewsPaper() { 195 NewsPaper.super.constructor.call(this); 196 this.content = ''; 197 } 198 199 extend(NewsPaper, Subject); 200 // 获取报纸的具体内容 201 NewsPaper.prototype.getContent = function () { 202 return this.content; 203 }; 204 // 设置报纸的具体内容,相当于要出版报纸了 205 NewsPaper.prototype.setContent = function (content) { 206 this.content = content; 207 // 内容有了,说明又出报纸了,那就通知所有的读者 208 this.notifyObservers(); 209 }; 210 211 // 观察者接口 212 function Observer() { 213 } 214 215 Observer.prototype.update = function () { 216 }; 217 218 219 function Reader() { 220 this.name = ''; 221 } 222 223 extend(Reader, Observer); 224 Reader.prototype.update = function (subject) { 225 console.log(this.name + '收到报纸了,阅读它,内容是:' + subject.getContent()); 226 }; 227 228 new function () { 229 var subject = new NewsPaper(); 230 231 var reader1 = new Reader(); 232 reader1.name = '张三'; 233 234 var reader2 = new Reader(); 235 reader2.name = '李四'; 236 237 var reader3 = new Reader(); 238 reader3.name = '王五'; 239 240 subject.attach(reader1); 241 subject.attach(reader2); 242 subject.attach(reader3); 243 244 subject.setContent('本期内容是观察者模式'); 245 }(); 246 }()); 247 248 (function () { 249 /** 250 * 推模型和拉模型 251 * 252 * 推模型 253 * 目标对象主动向观察者推送目标的详细信息,不管观察者是否需要,推送的信息通常是目标对象的全部或部分数据,相当于广播通信。 254 * 255 * 拉模型 256 * 目标对象在通知观察者的时候,值传递少量信息。如果观察者需要更具体的信息,有观察者主动到目标对象中获取,相当于是观察者从目标对象中拉数据。一般这种模式的实现中,会把目标对象自身通过update方法传递给观察者,这样在观察者需要获取数据的时候,就可以通过这个引用来获取了。 257 */ 258 259 /* 260 前面的例子就是典型的拉模型,下面看看推模型的实现 261 */ 262 // 订阅报纸,推模型 263 264 // 目标对象,作为被观察者,报社 265 function Subject() { 266 this.readers = []; 267 } 268 269 Subject.prototype = { 270 // 报纸的读者需要向报社订阅,先要注册 271 attach: function (reader) { 272 this.readers.push(reader); 273 }, 274 detach: function (reader) { 275 for (var i = 0, len = this.readers.length; i < len; i++) { 276 if (reader === this.readers[i]) { 277 this.readers.splice(i, 1); 278 return; 279 } 280 } 281 }, 282 /* 283 推模型的目标对象 284 跟拉模型的不同之处 285 1.通知所有观察者的方法,以前是没有参数的,现在需要传入需要主动推送的数据。 286 2.在循环通知观察者的时候,也就是循环调用观察者update方法的时候,传入的参数不同了 287 */ 288 // 当每期报纸印刷出来后,就要迅速主动地被送到读者的手中 289 // 相当于通知读者,让他们知道 290 /*------------------------------------*/ 291 notifyObservers: function (content) { 292 for (var i = 0, len = this.readers.length; i < len; i++) { 293 this.readers[i].update(content); 294 } 295 } 296 /*------------------------------------*/ 297 }; 298 299 // 报纸对象,具体的目标实现 300 function NewsPaper() { 301 NewsPaper.super.constructor.call(this); 302 this.content = ''; 303 } 304 305 extend(NewsPaper, Subject); 306 // 获取报纸的具体内容 307 NewsPaper.prototype.getContent = function () { 308 return this.content; 309 }; 310 // 设置报纸的具体内容,相当于要出版报纸了 311 /*------------------------------------*/ 312 NewsPaper.prototype.setContent = function (content) { 313 this.content = content; 314 // 内容有了,说明又出报纸了,那就通知所有的读者 315 this.notifyObservers(content); 316 }; 317 /*------------------------------------*/ 318 319 // 观察者接口 320 function Observer() { 321 } 322 323 Observer.prototype.update = function () { 324 }; 325 326 327 function Reader() { 328 this.name = ''; 329 } 330 331 extend(Reader, Observer); 332 // 推模型通常都是把需要传递的数据直接推送给观察者对象, 333 // 所以观察者接口中的update方法的参数需要变化。 334 /*------------------------------------*/ 335 Reader.prototype.update = function (content) { 336 // 以前需要到目标对象中获取自己需要的数据,现在是直接接受传入的数据 337 console.log(this.name + '收到报纸了,阅读它,内容是:' + content); 338 }; 339 /*------------------------------------*/ 340 341 new function () { 342 var subject = new NewsPaper(); 343 344 var reader1 = new Reader(); 345 reader1.name = '张三'; 346 347 var reader2 = new Reader(); 348 reader2.name = '李四'; 349 350 var reader3 = new Reader(); 351 reader3.name = '王五'; 352 353 subject.attach(reader1); 354 subject.attach(reader2); 355 subject.attach(reader3); 356 357 subject.setContent('本期内容是观察者模式'); 358 }(); 359 360 /* 361 应该使用哪种模型 362 363 1.推模型是假定目标对象知道观察者需要的数据;而拉模型是目标对象不知道观察者具体需要什么数据,没有办法的情况下,干脆把自身传给观察者,让观察者自己按需取值。 364 2.推模型可能会使得观察者对象难以复用,因为观察者定义的update方法是按需而定义的,可能无法兼顾所有情况。这意味着出现新情况的时候,就可能需要提供新的update方法,或者干脆重新实现观察者。 365 而拉模型就不会造成这样的情况,因为拉模型下,update方法的参数就、是目标对象本身,这基本上可以适应各种情况的需要。 366 */ 367 }()); 368 369 370 /* 371 示例:报纸的投送(JS pro design pattern) 372 373 在报纸行业中,发行和订阅的顺序进行有赖于一些关键性的角色和行为。首先是读者,他们都是订阅者(subscriber),他们消费数据并且根据读到的消息做出反应。。另一个角色是发行方(publisher),他们负责出版报纸。 374 确定了各方的身份之后,我们就可以分析每一方的职责所在。作为报纸的订阅者,,数据到来的时候我们收到通知,我们消费数据,然后我们根据数据做出反应,只要报纸到了订阅者手中,他们就可以自行处理。总而言之,订阅者要从发行方接收数据。 375 376 发行方则要发送数据。发行方也是投送房(deliver)。一般来说,一个发行方很可能有许多订阅者,同样,一个订阅者也很可能会订阅多家报社的报纸。问题的关键在于,这是一种多对多的关系,需要一种高级的抽象策略,以便订阅者能够彼此独立地发生变化,而发行方能够接受任何有消费意向的订阅者。 377 378 对于报社来说,只为给几个订阅者投送报纸就满世界跑是不划算的。而纽约市民也不可能特意飞到旧金山去拿自己订的报纸,要知道这份报纸可以直接投送到他们家门口。 379 订阅者要想拿到报纸的话有两种投送方式可选:推或拉。在推环境中,发行方很可能会雇佣投送人员四处送报。换句话说,他们把自己的报纸推出去,让订阅者收取。在拉环境中,规模较小的本地报社可能会在订阅者家附近的街角提供自己的数据,供订阅者“拉”。那么成长型发行方没有足够的资源进行大规模投送,因此采用拉方案,对于他们来说往往是个优化投送环节的好办法。 380 */ 381 382 /* 383 模式的实践 384 385 (1)订阅者可以订阅和退订,它们还要接受。它们可以在”由人投送(being delivered to)“和“自己收取(receiving )”之间进行选择(即推拉)。 386 (2)发布者负责投送,它们可以在“送出(giving)”和“由人取(being taken from)”之间进行选择 387 388 下面是一个展示发布者和订阅者之间的互动过程的高层示例。它是Sells方法的一个示例。这种技术类似于测试驱动的开发(TDD),不过它要求先写实现代码。 389 */ 390 391 function test() { 392 /* 393 Publishers are in charge of "publishing" i.e. creating the event 394 They;re alse in charge of "notifying" (firing the event) 395 */ 396 var Publisher = new Observable(); 397 398 /* 399 Subscribers basically ,,, "subscribe" (or listen). 400 Once they've been "notified" their callback functions are invoked. 401 */ 402 var Subscriber = function (news) { 403 // new delivered directly to my front porch 404 }; 405 Publisher.subscribeCustomer(Subscriber); 406 407 /* 408 Deliver a paper 409 sends out the new to all subscribers 410 */ 411 Publisher.deliver('extre, extre, read all about it'); 412 413 /* 414 That customer forget to pay his bill 415 */ 416 Publisher.unSubscribeCustomer(Subscriber); 417 /* 418 在这个模型中,可以看出发布者处于明显的主导地位。它们负责登记其顾客,而且有权停止为其投送。最后,新的报纸出版后它们会将其投送给顾客。 419 */ 420 421 // 下面的例子处理的事同一类问题,但发布者和订阅者之间的互动方式有所不同 422 /* 423 Newspaper vendors 424 setup as new Publisher obejcts 425 */ 426 var NewYorkTimes = new Publisher(); 427 var AustinHerald = new Publisher(); 428 var SfChronicle = new Publisher(); 429 430 /* 431 People who like to read 432 (Subscribers) 433 434 Each subscriber is set up as a callback method. 435 They all inherit from the Function prototype Object 436 */ 437 var Joe = function (from) { 438 console.log('Delivery from ' + from + ' to Joe'); 439 }; 440 var Lindsay = function (from) { 441 console.log('Delivery from ' + from + ' to Lindsay'); 442 }; 443 444 /* 445 Here we allow them to subscribe to newspaper which are the Publisher object. 446 */ 447 Joe. 448 subscribe(NewYorkTimes). 449 subscribe(SfChronicle); 450 Lindsay. 451 subscribe(AustinHerald). 452 subscribe(SfChronicle). 453 subscribe(NewYorkTimes); 454 455 /* 456 Then at any giving time in our application, our publishers can send off data for the subscribers to consume and react to. 457 */ 458 NewYorkTimes. 459 deliver('Here is your paper!direct from the Big apple'); 460 AustinHerald. 461 deliver('News'). 462 deliver('Reviews'). 463 deliver('Coupons'); 464 } 465 ; 466 467 /* 468 在这个例子中,发布者的创建方式和订阅者接收数据的方式没有多少改变,但拥有订阅和退订的一方变成了订阅者。当然,负责发送数据的还是发布者一方。 469 */ 470 471 // 扩展链式调用方法 472 Function.prototype.method = function (name, fn) { 473 this.prototype[name] = fn; 474 return this; 475 }; 476 477 // 扩展数组方法 478 if (!Array.prototype.forEach) { 479 Array.method('forEach', function (fn, thisObj) { 480 var scope = thisObj || window; 481 for (var i = 0, len = this.length; i < len; i++) { 482 fn.call(scope, this[i], i, this); 483 } 484 }); 485 } 486 487 if (!Array.prototype.filter) { 488 Array.method('filter', function (fn, thisObj) { 489 var scope = thisObj || window; 490 var a = []; 491 for (var i = 0, len = this.length; i < len; i++) { 492 if (!fn.call(scope, this[i], i, this)) { 493 continue; 494 } 495 a.push(this[i]); 496 } 497 return a; 498 }); 499 } 500 501 Array.prototype.some = Array.prototype.some || function (fn, context) { 502 for (var i = this, len = this.length; i < len; i++) { 503 if (fn.call(context)) { 504 return true; 505 } 506 } 507 return false; 508 }; 509 510 // demo: 511 function isBigEnough(element, index, array) { 512 return (element >= 10); 513 } 514 var filtered = [12, 5, 8, 130, 44].filter(isBigEnough); 515 // 12, 130, 44 516 517 518 // 添加观察者系统 519 window.DED = window.DED || {}; 520 DED.util = DED.util || {}; 521 DED.util.Observer = function () { 522 this.fns = []; 523 }; 524 DED.util.Observer.prototype = { 525 subscribe: function (fn) { 526 this.fns.push(fn); 527 }, 528 unsubscribe: function (fn) { 529 // 取消订阅当前函数 530 this.fns = this.fns.filter(function (el) { 531 if (el !== fn) { 532 return el; 533 } 534 }); 535 }, 536 fire: function (o) { 537 // 触发(运行)所有保存的函数 538 this.fns.forEach(function (el) { 539 el(o); 540 }); 541 } 542 }; 543 544 // 构建观察者API 545 function Publisher() { 546 this.subscribers = []; 547 } 548 // 投送方法 549 Publisher.prototype.deliver = function (data) { 550 this.subscribers.forEach(function (fn) { 551 fn(data); 552 }); 553 return this; 554 }; 555 // 订阅方法 556 Function.prototype.subscribe = function (publisher) { 557 var that = this; 558 var alreadyExists = publisher.subscribers.some(function (el) { 559 return el === that; 560 }); 561 if (!alreadyExists) { 562 publisher.subscribers.push(this); 563 } 564 return this; 565 }; 566 // 退订方法 567 Function.prototype.unsubscribe = function (publisher) { 568 var that = this; 569 publisher.subscribers = publisher.subscribers.filter(function (el) { 570 return el !== that; 571 }); 572 return this; 573 }; 574 575 // 有些订阅者在监听到某种一次性的事件之后会在回调阶段立即退订事件: 576 var publisherObject = new Publisher(); 577 var observerObject = function (data) { 578 // process data 579 console.log(data); 580 // unsubscribe from this publisher 581 arguments.callee.unsubscribe(publisherObject); 582 }; 583 observerObject.subscribe(publisherObject); 584 publisherObject.deliver('This is news'); 585 586 587 // 示例:动画 588 // Publisher API 589 var Animation = function (o) { 590 this.onStart = new Publisher(); 591 this.onComplete = new Publisher(); 592 this.onTween = new Publisher(); 593 }; 594 Animation.prototype = { 595 fly: function () { 596 // begin animation 597 this.onStart.deliver(); 598 /* 599 for(...) { // loop through frames 600 //deliver frame number 601 this.onTween.deliver(i); 602 } 603 */ 604 // end animation 605 this.onComplete.deliver(); 606 } 607 }; 608 609 // setup an account with the animation manager 610 var superman = new Animation({ 611 // config properties 612 }); 613 614 // Begin implementing subscribers 615 var putOnCape = function (i) { 616 }; 617 var takeOffCape = function (i) { 618 }; 619 620 putOnCape.subscribe(superman.onStart); 621 takeOffCape.subscribe(superman.onComplete); 622 623 function main() { 624 // fly can be called anywhere 625 superman.fly(); 626 // for instance 627 addEvent(element, 'click', function () { 628 superman.fly(); 629 }); 630 } 631 632 (function () { 633 // 变形示例,通知相应的观察者或部分观察者 634 635 function WatcherObserver() { 636 this.job = ''; 637 } 638 639 WatcherObserver.prototype = { 640 update: function (subject) { 641 // 拉模型 642 console.log(this.job + '获取到通知,当前污染级别为:' + subject.getPolluteLevel()); 643 } 644 }; 645 646 function WaterQualitySubject() { 647 this.polluteLevel = 0; 648 this.observers = []; 649 } 650 651 WaterQualitySubject.prototype = { 652 attach: function (observer) { 653 this.observers.push(observer); 654 }, 655 detach: function (observer) { 656 for (var i = 0, len = this.observers.length; i < len; i++) { 657 if (observer === this.observers[i]) { 658 this.observers.splice(i, 1); 659 return; 660 } 661 } 662 }, 663 notifyWatchers: function () { 664 var watcher; 665 for (var i = 0, len = this.observers.length; i < len; i++) { 666 watcher = this.observers[i]; 667 if (this.polluteLevel >= 0) { 668 if (watcher.job === '检测人员') { 669 watcher.update(this); 670 } 671 } 672 673 if (this.polluteLevel >= 1) { 674 if (watcher.job === '预警人员') { 675 watcher.update(this); 676 } 677 } 678 679 if (this.polluteLevel >= 2) { 680 if (watcher.job === '检测部门领导') { 681 watcher.update(this); 682 } 683 } 684 } 685 }, 686 getPolluteLevel: function () { 687 return this.polluteLevel; 688 }, 689 setPolluteLevel: function (level) { 690 this.polluteLevel = level; 691 this.notifyWatchers(); 692 } 693 }; 694 695 new function () { 696 var subject = new WaterQualitySubject(); 697 var watch1 = new WatcherObserver(); 698 watch1.job = '检测人员'; 699 var watch2 = new WatcherObserver(); 700 watch2.job = '预警人员'; 701 var watch3 = new WatcherObserver(); 702 watch3.job = '检测部门领导'; 703 704 subject.attach(watch1); 705 subject.attach(watch2); 706 subject.attach(watch3); 707 708 console.log('当水质为正常的时候--------------------'); 709 subject.setPolluteLevel(0); 710 console.log('当水质为轻度的时候--------------------'); 711 subject.setPolluteLevel(1); 712 console.log('当水质为中度的时候--------------------'); 713 subject.setPolluteLevel(2); 714 }(); 715 }()); 716 717 /** 718 * 优点 719 * 720 * 1.观察者模式实现了观察者和目标之间的抽象耦合 721 * 原本目标对象在状态发生改变的时候,需要直接调用所有的观察者对象,但是抽象出观察者接口以后,目标和观察者就只是在抽象层面上耦合了,也就是说目标只是知道观察者接口,并不知道具体的观察者的类,从而实现目标类和具体的观察者类之间解耦。 722 * 723 * 2.观察者模式实现了动态联动 724 * 所谓联动,就是做一个操作会引起其他相关的操作。由于观察者模式对观察者注册实现管理,那就可以在运行期间,通过动态地控制注册的观察者,来控制某个动作的联动范围,从而实现动态联动。 725 * 726 * 3.观察者模式支持广播通信 727 * 由于目标发送通知给观察者是面向所有注册的观察者,所以每次目标通知的信息就要对所有注册的观察者进行广播。当然,也可以通过在目标上添加新的功能来限制广播的范围。 728 * 在广播通信的时候要注意一个问题,就是相互广播造成死循环的问题。比如A和B两个对象互为观察者和目标对象。 729 * 730 * 缺点 731 * 732 * 1.可能会引起无谓的操作 733 * 由于观察者模式每次都是广播通信,不管观察者需不需要,每个观察者都会被调用update方法,如果观察者不需要执行相应处理,那么这次操作就浪费了,甚至引起误更新。比如,本应该在执行这次状态更新前把某个观察者删除掉,这样通知的时候就没有这个观察者了,但是却忘了,那么就会引起误操作。 734 * 735 * 相关模式 736 * 737 * 观察者模式和状态模式 738 * 有相似之处。 739 * 观察者模式是当目标状态发生改变时,触发并通知观察者,让观察者去执行相应的操作。而状态模式是根据不同的状态,选择不同的实现,这个现实类的主要功能就是针对状态相应地操作,它不像观察者,观察者本身还有很多其他的功能,接收通知并执行相应处理知识观察者的部分功能。 740 * 当然观察者模式和状态模式是可以结合使用的。观察者模式的重心在触发联动,但是到底决定哪些观察者会被联动,这时就可以采用状态模式来实现了,也可以采用策略模式来进行选择需要联动的观察者。 741 * 742 * 观察者模式和中介者模式 743 * 可以结合使用。 744 * 如果观察者和被观察者之间的交互关系很复杂,比如,省级三级联动。这种情况下,很明显需要相关的状态都联动准备好了,然后再一次性地通知观察者。可以使用中介者模式来封装观察者和目标的关系。 745 * 在应用里面,比如,把一个界面所有的事件用一个对象来处理,把一个组件触发事件后,需要操作其他组件的动作都封装到一起,这个对象就是中介者了。 746 */ 747 748 // Javscript Patterns example: 749 /* Title: Observer 750 Description: is a publish/subscribe pattern which allows a number of observer objects to see an event 751 */ 752 var Observer = { 753 addSubscriber: function (callback) { 754 this.subscribers[this.subscribers.length] = callback; 755 }, 756 removeSubscriber: function (callback) { 757 for (var i = 0; i < this.subscribers.length; i++) { 758 if (this.subscribers[i] === callback) { 759 delete this.subscribers[i]; 760 } 761 } 762 }, 763 publish: function () { 764 for (var i = 0; i < this.subscribers.length; i++) { 765 if (typeof this.subscribers[i] === 'function') { 766 this.subscribers[i].apply(this, arguments); 767 } 768 } 769 }, 770 make: function (o) { 771 for (var i in this) { 772 o[i] = this[i]; 773 o.subscribers = []; 774 } 775 } 776 }; 777 778 var blogger = { 779 writeBlogPost: function () { 780 var content = 'Today is ' + new Date(); 781 this.publish(content); 782 } 783 }; 784 785 var la_times = { 786 newIssue: function () { 787 var paper = 'Martians have landed on Earth.'; 788 this.publish(paper); 789 } 790 }; 791 792 Observer.make(blogger); 793 Observer.make(la_times); 794 795 var jack = { 796 read: function (what) { 797 console.log('I just read that ' + what); 798 } 799 }; 800 801 var jill = { 802 gossip: function (what) { 803 console.log('You didn\'t hear it from me, but' + what); 804 } 805 }; 806 807 blogger.addSubscriber(jack.read); 808 blogger.addSubscriber(jill.gossip); 809 blogger.writeBlogPost(); 810 811 blogger.removeSubscriber(jill.gossip); 812 blogger.writeBlogPost(); 813 814 la_times.addSubscriber(jill.gossip); 815 la_times.newIssue(); 816 817 818 // http://www.joezimjs.com/javascript/javascript-design-patterns-observer/ 819 // Now we’ll implement the pull method of the observer pattern. 820 // When you’re using the pull method, 821 // it makes more sense to swap things around a bit: 822 Observable = function () { 823 this.status = "constructed"; 824 } 825 Observable.prototype.getStatus = function () { 826 return this.status; 827 } 828 829 Observer = function () { 830 this.subscriptions = []; 831 } 832 Observer.prototype = { 833 subscribeTo: function (observable) { 834 this.subscriptions.push(observable); 835 }, 836 unsubscribeFrom: function (observable) { 837 var i = 0, 838 len = this.subscriptions.length; 839 840 // Iterate through the array and if the observable is 841 // found, remove it. 842 for (; i < len; i++) { 843 if (this.subscriptions[i] === observable) { 844 this.subscriptions.splice(i, 1); 845 // Once we've found it and removed it, we 846 // don't need to continue, so just return. 847 return; 848 } 849 } 850 }, 851 doSomethingIfOk: function () { 852 var i = 0, 853 len = this.subscriptions.length; 854 855 // Iterate through the subscriptions and determine 856 // whether the status has changed to ok on each of them, 857 // and do something for each subscription that has 858 for (; i < len; i++) { 859 if (this.subscriptions[i].getStatus() === "ok") { 860 // Do something because the status of the 861 // observable is what we want it to be 862 } 863 } 864 } 865 } 866 867 var observer = new Observer(), 868 observable = new Observable(); 869 observer.subscribeTo(observable); 870 871 // Nothing will happen because the status hasn't changed 872 observer.doSomethingIfOk(); 873 874 // Change the status to "ok" so now something will happen 875 observable.status = "ok"; 876 observer.doSomethingIfOk(); 877 878 </script> 879 880 <select name="country" id="country"> 881 <option value="01">01</option> 882 <option value="02">02</option> 883 <option value="03">03</option> 884 <option value="04">04</option> 885 <option value="05">05</option> 886 </select> 887 <select name="province" id="province"> 888 <option value="001">001</option> 889 <option value="002">002</option> 890 <option value="003">003</option> 891 <option value="004">004</option> 892 <option value="005">005</option> 893 </select> 894 <select name="city" id="city"> 895 <option value="0001">0001</option> 896 <option value="0002">0002</option> 897 <option value="0003">0003</option> 898 <option value="0004">0004</option> 899 <option value="0005">0005</option> 900 </select> 901 902 <script> 903 (function () { 904 // 观察者模式和中介者模式组合使用 905 906 var Mediator = { 907 changed: function (colleague) { 908 switch (colleague.element.id) { 909 case 'country': 910 this.logProvinceChange(); 911 break; 912 case 'province': 913 this.logCityChange(); 914 break; 915 } 916 }, 917 logProvinceChange: function () { 918 console.log('省份联动了,当前值为' + this.province.element.value); 919 }, 920 logCityChange: function () { 921 console.log('市联动了,当前值为' + this.city.element.value); 922 } 923 }; 924 925 function CountrySelect(element, mediator) { 926 this.observers = []; 927 this.element = element; 928 this.mediator = mediator; 929 var me = this; 930 this.element.addEventListener('change', function (e) { 931 me.notifyObservers(); 932 }, false); 933 } 934 935 CountrySelect.prototype = { 936 attach: function (observer) { 937 this.observers.push(observer); 938 }, 939 detach: function (observer) { 940 for (var i = 0, len = this.observers.length; i < len; i++) { 941 if (observer === this.observers[i]) { 942 this.observers.splice(i, 1); 943 return; 944 } 945 } 946 }, 947 notifyObservers: function () { 948 for (var i = 0, len = this.observers.length; i < len; i++) { 949 this.observers[i].update(this); 950 } 951 952 this.mediator.changed(this); 953 } 954 }; 955 956 function ProvinceSelect(element, mediator) { 957 this.observers = []; 958 this.element = element; 959 this.mediator = mediator; 960 var me = this; 961 this.element.addEventListener('change', function (e) { 962 me.notifyObservers(); 963 }, false); 964 } 965 966 ProvinceSelect.prototype = { 967 attach: function (observer) { 968 this.observers.push(observer); 969 }, 970 detach: function (observer) { 971 for (var i = 0, len = this.observers.length; i < len; i++) { 972 if (observer === this.observers[i]) { 973 this.observers.splice(i, 1); 974 return; 975 } 976 } 977 }, 978 notifyObservers: function () { 979 for (var i = 0, len = this.observers.length; i < len; i++) { 980 this.observers[i].update(this); 981 } 982 983 this.mediator.changed(this); 984 }, 985 986 update: function (subject) { 987 var index = parseInt(subject.element.value, 10) - 1; 988 this.element.selectedIndex = index; 989 990 this.notifyObservers(); 991 } 992 }; 993 994 function CitySelect(element) { 995 this.element = element; 996 } 997 998 CitySelect.prototype = { 999 update: function (subject) { 1000 var index = parseInt(subject.element.value, 10) - 1; 1001 this.element.selectedIndex = index; 1002 } 1003 }; 1004 1005 var country = new CountrySelect(document.getElementById('country'), Mediator); 1006 var province = new ProvinceSelect(document.getElementById('province'), Mediator); 1007 var city = new CitySelect(document.getElementById('city')); 1008 1009 Mediator.province = province; 1010 Mediator.city = city; 1011 1012 country.attach(province); 1013 province.attach(city); 1014 }()); 1015 </script> 1016 </body> 1017 </html>