发布-订阅模式 又叫 观察者模式。 它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
在JavaScript开发中,我们一般用事件模型来替代传统的发布订阅模式。
①发布—订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。比如,我们可以订阅ajax请求的error、succ等事件。
②可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。
买房者收到售楼处发布的房源售卖通知。售楼处就是发布者,买房者就是订阅者,订阅的是售卖房源的信息。买房者有很多人,他们会订阅不同的房源(不同的地理位置,价格、周边等),发布者可以根据不同的订阅者发布不同的信息。
- 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 );
- var fns = this.clientList[ key ];
- if ( ! fns || fns.length === 0 ) {
- return false;
- }
- for ( var i=0, fn; fn = fns[i++]; ) {
- fn.apply( this, arguments );
- }
- };
- ***********************************************************************
- var clientA = { // A订阅的信息是88平米的价格
- key: 'squareMeter88',
- fn: function( price ){
- console.log('The squareMeter of 88 is :' + price +' RMB.')
- }
- };
- var clientB = { // B订阅的信息是110平米的价格
- key: 'squareMeter110',
- fn: function( price ){
- console.log('The squareMeter of 110 is :' + price +' RMB.')
- }
- };
- //收到用户的订阅
- salesOffices.listen( clientA.key, clientA.fn );
- salesOffices.listen( clientB.key, clientB.fn );
-
- //售楼开始发布房源
- salesOffices.trigger('squareMeter88', '1800000' );
- salesOffices.trigger('squareMeter110', '2500000' );
我们可以把发布订阅的功能提取为一个对象,再定义一个 安装函数,这样其他售楼处也可以安装这个功能。给 发布订阅对象 PandS 添加一个 remove 方法,取消订阅的事件。
- // 发布订阅对象
- var PandS = {
- clientList: [], // 一个客户列表
- listen: function( key, fn ) {……}, // 一个收集用户订阅信息的方法
- trigger: function() {……}, // 一个发布订阅消息给用户的方法
-
- remove: function( key, fn ) { // 增加一个删除订阅事件的方法
- var fns = this.clientList[ key ];
- if ( ! fns ) { return false; } // 如果没有被订阅,则直接返回
- if ( ! fn ) { // 如果没有传 fn, 则表示 要取消 key 所有的订阅事件
- fns && ( fns.length = 0 );
- } else {
- for (var len = fns.length -1; len >= 0; len-- ) {
- if ( fns[ len ] === fn ) {
- fns.splice( len, 1 );
- }
- }
- }
- }
- };
- // 安装函数
- var installPandS = function( obj ) {
- for( var i in PandS ) {
- obj[ i ] = PandS[ i ]; // 复制对象的属性和方法,即安装
- }
- };
- //测试:售楼处REUB 安装 发布订阅功能
- var REUB = {};
- installPandS( REUB ); // 安装好了
- REUB.listen( clientA.key, clientA.fn );
- REUB.trigger('squareMeter88', '1800000' );
- REUB.remove( clientA.key, clientA.fn ); // 删除A的订阅
网站登录 实例:
- var login = { // 一个叫 login 的发布者
- clientList: [],
- listen: function( client ) {……},
- trigger: function() {……},
- remove: function( client ) {……}
- };
- $.ajax('http://xxx.com?login', function(data) {
- login.trigger('loginSuccess', data); // 发布登陆成功消息
- });
- // header模块订阅
- var header = (function(){
- // 用户订阅登录成功信息
- login.listen( {
- key: 'loginSuccess',
- fn: function( data) {
- header.setAvatar( data.avatar );
- }
- } );
- return {
- setAvatar: function( data ) {
- console.log('设置header 头像: ', data );
- }
- }
- })();
- // nav 模块订阅 略
总结:我们随时可以把setAvatar的方法名改成setTouxiang。如果有一天在登录完成之后,又增加一个刷新收货地址列表的行为,那么只要在收货地址模块里加上监听消息的方法即可,而这可以让开发该模块的同事自己完成,你作为登录模块的开发者,永远不用再关心这些行为。
先发布后订阅 与 命名冲突
前面的代码都是只能 先订阅某个事件,发布必须在其后,否则无法获取到真正的订阅。为了解决这个问题,可以使用一个存放离线事件的堆栈,当发布事件的时候,将动作包裹在函数里,将函数存入堆栈中。等到终于有对象来订阅此事件的时候,我们将遍历堆栈并且依次执行这些包装函数,也就是重新发布里面的事件。
将 发布订阅对象独立出来,取名为 Event。里面只有 clientList 来存放消息名和回调函数,为了避免冲突,给 Event对象提供创建命名空间的功能。
- // 先发布后订阅 效果
- Event.trigger( 'click', '123' );
- Event.listen( 'click', function(a) { console.log(a); } ); // 输出 '123'
-
- // 命名空间效果
- Event.create('name1').listen( 'click', function(a) { console.log(a); } ); // 输出 '123'
- Event.create('name1').trigger( 'click', '123' );
-
- Event.create('name2').listen( 'click', function(a) { console.log(a); } ); // 输出 '456'
- Event.create('name2').trigger( 'click', '456' );
实现 略。(详细参见:JavaScript设计模式与开发实践 8.11节)
发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。它的应用非常广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。发布—订阅模式还可以用来帮助实现一些别的设计模式,比如中介者模式。从架构上来看,无论是MVC还是MVVM,都少不了发布—订阅模式的参与,而且JavaScript本身也是一门基于事件驱动的语言。
创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。
手写一个发布订阅:
- // 发布订阅中心, on-订阅, off取消订阅, emit发布, 内部需要一个单独事件中心caches进行存储;
- interface CacheProps {
- [key: string]: Array<((data?: unknown) => void)>;
- }
-
- class Observer {
- private caches: CacheProps = {}; // 事件中心
- on (eventName: string, fn: (data?: unknown) => void){ // eventName事件名-独一无二, fn订阅后执行的自定义行为
- this.caches[eventName] = this.caches[eventName] || [];
- this.caches[eventName].push(fn);
- }
-
- emit (eventName: string, data?: unknown) { // 发布 => 将订阅的事件进行统一执行
- if (this.caches[eventName]) {
- this.caches[eventName].forEach((fn: (data?: unknown) => void) => fn(data));
- }
- }
-
- off (eventName: string, fn?: (data?: unknown) => void) { // 取消订阅 => 若fn不传, 直接取消该事件所有订阅信息
- if (this.caches[eventName]) {
- const newCaches = fn ? this.caches[eventName].filter(e => e !== fn) : [];
- this.caches[eventName] = newCaches;
- }
- }
- }