为什么要使用发布/订阅模式(在JS/jQuery中)?

Mac*_*ath 99 javascript jquery design-patterns publish-subscribe

所以,一位同事向我介绍了发布/订阅模式(在JS/jQuery中),但是我很难掌握为什么会使用这种模式而不是'普通'的JavaScript/jQuery.

例如,之前我有以下代码......

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    var orders = $(this).parents('form:first').find('div.order');
    if (orders.length > 2) {
        orders.last().remove();
    }
});
Run Code Online (Sandbox Code Playgroud)

我可以看到这样做的优点,例如......

removeOrder = function(orders) {
    if (orders.length > 2) {
        orders.last().remove();
    }
}

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    removeOrder($(this).parents('form:first').find('div.order'));
});
Run Code Online (Sandbox Code Playgroud)

因为它引入了removeOrder为不同事件等重用功能的能力.

但是,为什么你决定实现发布/订阅模式并转到以下长度,如果它做同样的事情?(仅供参考,我使用jQuery tiny pub/sub)

removeOrder = function(e, orders) {
    if (orders.length > 2) {
        orders.last().remove();
    }
}

$.subscribe('iquery/action/remove-order', removeOrder);

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    $.publish('iquery/action/remove-order', $(this).parents('form:first').find('div.order'));
});
Run Code Online (Sandbox Code Playgroud)

我肯定已经读过关于这种模式的内容,但我无法想象为什么这种模式是必要的.我看过的解释如何实现这种模式的教程只是作为我自己的基本示例.

我认为pub/sub的有用性会在更复杂的应用程序中显现出来,但我无法想象.我担心我完全忽略了这一点; 但是如果有的话,我想知道这一点!

你能简洁地解释为什么以及在什么情况下这种模式是有利的?是否值得像上面的例子那样使用pub/sub模式进行代码片段?

Min*_*hev 218

这一切都与松散耦合和单一责任有关,它与JavaScript中的MV*(MVC/MVP/MVVM)模式密切相关,这些模式在过去几年中非常现代.

松耦合是一种面向对象的原则,其中系统的每个组件都知道它的责任并且不关心其他组件(或者至少尽量不关心它们).松散耦合是一件好事,因为您可以轻松地重用不同的模块.您没有与其他模块的接口耦合.使用发布/订阅你只需要发布/订阅接口,这不是一个大问题 - 只有两种方法.因此,如果您决定在不同的项目中重复使用模块,您可以复制并粘贴它,它可能会起作用,或者至少您不需要太多努力就可以使它工作.

在谈到松散耦合时,我们应该提到关注点分离.如果您使用MV*体系结构模式构建应用程序,则始终具有模型和视图.模型是应用程序的业务部分.您可以在不同的应用程序中重用它,因此将它与要显示它的单个应用程序的视图结合起来并不是一个好主意,因为通常在不同的应用程序中您有不同的视图.因此,使用发布/订阅进行模型 - 视图通信是个好主意.当您的模型更改它发布事件时,View会捕获它并自行更新.您没有发布/订阅的任何开销,它可以帮助您进行解耦.以同样的方式,您可以将应用程序逻辑保留在Controller中(例如MVVM,MVP,它不是一个控制器),并使View尽可能简单.当您的视图发生更改(或者用户点击某些内容)时,它只会发布一个新事件,Controller会捕获它并决定要执行的操作.如果你熟悉的话MVC模式或Microsoft技术中的MVVM(WPF/Silverlight)您可以将发布/订阅视为Observer模式.这种方法用于Backbone.js,Knockout.js(MVVM)等框架.

这是一个例子:

//Model
function Book(name, isbn) {
    this.name = name;
    this.isbn = isbn;
}

function BookCollection(books) {
    this.books = books;
}

BookCollection.prototype.addBook = function (book) {
    this.books.push(book);
    $.publish('book-added', book);
    return book;
}

BookCollection.prototype.removeBook = function (book) {
   var removed;
   if (typeof book === 'number') {
       removed = this.books.splice(book, 1);
   }
   for (var i = 0; i < this.books.length; i += 1) {
      if (this.books[i] === book) {
          removed = this.books.splice(i, 1);
      }
   }
   $.publish('book-removed', removed);
   return removed;
}

//View
var BookListView = (function () {

   function removeBook(book) {
      $('#' + book.isbn).remove();
   }

   function addBook(book) {
      $('#bookList').append('<div id="' + book.isbn + '">' + book.name + '</div>');
   }

   return {
      init: function () {
         $.subscribe('book-removed', removeBook);
         $.subscribe('book-aded', addBook);
      }
   }
}());
Run Code Online (Sandbox Code Playgroud)

另一个例子.如果你不喜欢MV*方法,你可以使用一些不同的东西(我将在下面描述的那个和最后提到的那个之间有一个交集).只需在不同的模块中构建应用程序.例如,看看Twitter.

Twitter模块

如果你看一下界面,你就会有不同的盒子.您可以将每个框视为不同的模块.例如,您可以发布推文.此操作需要更新几个模块.首先,它必须更新您的个人资料数据(左上方框),但它还必须更新您的时间表.当然,您可以保留对这两个模块的引用并使用它们的公共接口单独更新它们,但只是发布一个事件更容易(也更好).这将使您的应用程序的修改更容易,因为更松散的耦合.如果您开发依赖于新推文的新模块,您只需订阅"发布 - 推文"事件并处理它.这种方法非常有用,可以使您的应用程序非常分离.您可以非常轻松地重用模块.

这是最后一种方法的基本示例(这不是原始的推特代码,它只是我的样本):

var Twitter.Timeline = (function () {
   var tweets = [];
   function publishTweet(tweet) {
      tweets.push(tweet);
      //publishing the tweet
   };
   return {
      init: function () {
         $.subscribe('tweet-posted', function (data) {
             publishTweet(data);
         });
      }
   };
}());


var Twitter.TweetPoster = (function () {
   return {
       init: function () {
           $('#postTweet').bind('click', function () {
               var tweet = $('#tweetInput').val();
               $.publish('tweet-posted', tweet);
           });
       }
   };
}());
Run Code Online (Sandbox Code Playgroud)

对于这种方法,尼古拉斯扎卡斯有一个很好的演讲.对于MV*方法,我所知道的最好的文章和书籍由Addy Osmani出版.

缺点:您必须小心过度使用发布/订阅.如果您有数百个事件,那么管理所有事件会变得非常混乱.如果您没有使用命名空间(或者没有以正确的方式使用它),您可能还会发生冲突.可以在https://github.com/ajacksified/Mediator.js找到Mediator的高级实现,它看起来很像发布/订阅.它具有命名空间和功能,如事件"冒泡",当然,可以中断.发布/订阅的另一个缺点是硬单元测试,可能难以隔离模块中的不同功能并独立地测试它们.

  • 谢谢,这是有道理的.我熟悉MVC模式,因为我一直使用PHP,但我没有考虑过事件驱动编程.:) (3认同)
  • 谢谢你的描述.真的帮助我围绕这个概念. (2认同)

And*_*rpi 16

主要目标是减少代码之间的耦合.这是一种基于事件的思维方式,但"事件"并不依赖于特定的对象.

我将在下面的一些伪代码中写出一个很大的例子,看起来有点像JavaScript.

假设我们有一个类Radio和一个类Relay:

class Relay {
    function RelaySignal(signal) {
        //do something we don't care about right now
    }
}

class Radio {
    function ReceiveSignal(signal) {
        //how do I send this signal to other relays?
    }
}
Run Code Online (Sandbox Code Playgroud)

无线电收到信号时,我们希望一些继电器以某种方式中继信息.继电器的数量和类型可以不同.我们可以这样做:

class Radio {
    var relayList = [];

    function AddRelay(relay) {
        relayList.add(relay);
    }

    function ReceiveSignal(signal) {
        for(relay in relayList) {
            relay.Relay(signal);
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

这很好用.但现在想象我们想要一个不同的组件也可以接收Radio类接收到的信号,即扬声器:

(对不起,如果类比不是一流......)

class Speakers {
    function PlaySignal(signal) {
        //do something with the signal to create sounds
    }
}
Run Code Online (Sandbox Code Playgroud)

我们可以再次重复这种模式:

class Radio {
    var relayList = [];
    var speakerList = [];

    function AddRelay(relay) {
        relayList.add(relay);
    }

    function AddSpeaker(speaker) {
        speakerList.add(speaker)
    }

    function ReceiveSignal(signal) {

        for(relay in relayList) {
            relay.Relay(signal);
        }

        for(speaker in speakerList) {
            speaker.PlaySignal(signal);
        }

    }

}
Run Code Online (Sandbox Code Playgroud)

我们可以通过创建一个接口来实现这一点,比如"SignalListener",这样我们在Radio类中只需要一个列表,并且总是可以在我们想要监听信号的任何对象上调用相同的函数.但是,这仍然会在我们决定的任何接口/基类/等与Radio类之间产生耦合.基本上每当你改变Radio,Signal或Relay类中的一个时,你必须考虑它可能如何影响其他两个类.

现在让我们尝试不同的东西.让我们创建一个名为RadioMast的第四个类:

class RadioMast {

    var receivers = [];

    //this is the "subscribe"
    function RegisterReceivers(signaltype, receiverMethod) {
        //if no list for this type of signal exits, create it
        if(receivers[signaltype] == null) {
            receivers[signaltype] = [];
        }
        //add a subscriber to this signal type
        receivers[signaltype].add(receiverMethod);
    }

    //this is the "publish"
    function Broadcast(signaltype, signal) {
        //loop through all receivers for this type of signal
        //and call them with the signal
        for(receiverMethod in receivers[signaltype]) {
            receiverMethod(signal);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我们有一个我们知道的模式,只要它们可以用于任何数量和类型的类:

  • 知道RadioMast(处理所有消息传递的类)
  • 知道发送/接收消息的方法签名

所以我们将Radio类改为最终的简单形式:

class Radio {
    function ReceiveSignal(signal) {
        RadioMast.Broadcast("specialradiosignal", signal);
    }
}
Run Code Online (Sandbox Code Playgroud)

我们将扬声器和继电器添加到RadioMast的接收器列表中以获取此类信号:

RadioMast.RegisterReceivers("specialradiosignal", speakers.PlaySignal);
RadioMast.RegisterReceivers("specialradiosignal", relay.RelaySignal);
Run Code Online (Sandbox Code Playgroud)

现在,Speakers和Relay类没有任何知识,除了他们有一个可以接收信号的方法,作为发布者的Radio类知道它发布信号的RadioMast.这是使用像发布/订阅这样的消息传递系统的关键.

  • JavaScript没有`class`关键字.请强调这一事实,例如.通过将代码分类为伪代码. (3认同)

Tre*_*hek 5

其他答案在展示模式如何工作方面做得很好.我想解决隐含的问题" 旧方式出了什么问题 ",因为我最近一直在使用这种模式,我发现这涉及到我的想法的转变.

想象一下,我们订阅了一份经济公报.该公告发布了一个标题:" 将道琼斯指数降低200点 ".发送这将是一个奇怪的,有点不负责任的消息.然而,如果它发表:" 安然今天早上申请破产保护第11章 ",那么这是一个更有用的信息.请注意,该消息可能导致道琼斯指数下跌200点,但这是另一回事.

发送命令和建议刚刚发生的事情之间存在差异.考虑到这一点,采用原始版本的pub/sub模式,暂时忽略处理程序:

$.subscribe('iquery/action/remove-order', removeOrder);

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    $.publish('iquery/action/remove-order', $(this).parents('form:first').find('div.order'));
});
Run Code Online (Sandbox Code Playgroud)

在这里,用户操作(单击)和系统响应(被删除的订单)之间已存在隐含的强耦合.有效地在你的例子中,行动是给出一个命令.考虑这个版本:

$.subscribe('iquery/action/remove-order-requested', handleRemoveOrderRequest);

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    $.publish('iquery/action/remove-order-requested', $(this).parents('form:first').find('div.order'));
});
Run Code Online (Sandbox Code Playgroud)

现在,处理程序正在响应已发生的某些兴趣,但没有义务删除订单.实际上,处理程序可以执行与删除订单无直接关系的各种事情,但仍可能与调用操作相关.例如:

handleRemoveOrderRequest = function(e, orders) {
    logAction(e, "remove order requested");
    if( !isUserLoggedIn()) {
        adviseUser("You need to be logged in to remove orders");
    } else if (isOkToRemoveOrders(orders)) {
        orders.last().remove();
        adviseUser("Your last order has been removed");
        logAction(e, "order removed OK");
    } else {
        adviseUser("Your order was not removed");
        logAction(e, "order not removed");
    }
    remindUserToFloss();
    increaseProgrammerBrowniePoints();
    //etc...
}
Run Code Online (Sandbox Code Playgroud)

IMO与命令和通知之间的区别是有用的区别.