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

所以,一位同事向我介绍了发布/订阅模式(在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(); } }); 

我可以看到这样做的好处,例如…

 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')); }); 

因为它引入了对不同事件removeOrderfunction的能力

但是,为什么你会决定实施发布/订阅模式,如果它做了同样的事情,那么去下面的长度? (仅供参考,我用jQuery的小酒馆/子 )

 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')); }); 

我已经读了关于这个模式,但我无法想象为什么这将是必要的。 我见过的教程解释了如何实现这个模式,只包含我自己的基本示例。

我想这个pub / sub的用处会使它在一个更复杂的应用程序中变得明显,但我无法想象它。 恐怕我完全错过了这一点。 但是如果有的话我想知道点!

你能简洁地解释为什么这种模式有利吗? 是否值得使用pub / sub模式的代码片段像我上面的例子?

这完全是关于松散的耦合和单一的责任,这与MV(MVC / MVP / MVVM)模式在近几年来非常现代化的MVC(MVC / MVP / MVVM)模式紧密相关。

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

当谈到松耦合时,我们应该提到关注的分离 。 如果您使用MV *架构模式构build应用程序,则始终有一个模型和一个视图。 模型是应用程序的业务部分。 您可以在不同的应用程序中重复使用它,因此将其与单个应用程序的视图(您想要显示它的视图)结合起来不是一个好主意,因为通常在不同的应用程序中您有不同的视图。 因此,使用发布/订阅模型视图通信是一个好主意。 当你的模型改变它发布一个事件,视图捕获它并自我更新。 你没有从发布/订阅任何开销,它可以帮助你解耦。 以同样的方式,你可以保持你的应用程序逻辑在控制器例如(MVVM,MVP它不完全是一个控制器),并保持视图尽可能简单。 当你的视图改变(或者用户点击某些东西),它只是发布一个新的事件,控制器抓住它,并决定要做什么。 如果您熟悉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); } } }()); 

另一个例子。 如果你不喜欢MV *方法,你可以使用一些不同的东西(我将介绍下一个和最后一个)。 只需在不同的模块中构build应用程序。 例如看看Twitter。

Twitter模块

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

这里是最后一个方法的基本例子(这不是原始的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); }); } }; }()); 

对于这种方法, 尼古拉斯·扎卡斯 ( Nicholas Zakas)发表了精彩的演讲 对于MV *方法,我所知道的最好的文章和书籍由Addy Osmani出版。

缺点:您必须小心过度使用发布/订阅。 如果你有数百个事件,pipe理所有事件会变得非常混乱。 如果你不使用命名空间(或者没有以正确的方式使用它),你也可能会碰撞。 Mediator的高级实现看起来很像发布/订阅可以在这里findhttps://github.com/ajacksified/Mediator.js 。 它具有命名空间和function,如事件“冒泡”,当然,可以被打断。 发布/订阅的另一个缺点是硬件unit testing,可能难以将模块中的不同function隔离开来并单独进行testing。

主要目标是减less代码之间的耦合。 这是一种基于事件的思维方式,但是“事件”并不是绑定到某个特定的对象上的。

我将在下面的一些大的例子中看到一些类似JavaScript的伪代码。

比方说,我们有一个class级电台和一个class级中继:

 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? } } 

每当无线电收到信号,我们想要一些继电器以某种方式中继该信息。 继电器的数量和types可能不同。 我们可以这样做:

 class Radio { var relayList = []; function AddRelay(relay) { relayList.add(relay); } function ReceiveSignal(signal) { for(relay in relayList) { relay.Relay(signal); } } } 

这工作正常。 但是现在想象一下,我们想要一个不同的组件也能够接收Radio类收到的信号,即Speakers:

(对不起,如果类比不是一stream的…)

 class Speakers { function PlaySignal(signal) { //do something with the signal to create sounds } } 

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

 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); } } } 

我们可以通过创build一个接口(比如“SignalListener”)来使其更好,这样我们只需要Radio类中的一个列表,并且总是可以调用与我们想要监听信号的对象相同的函数。 但是,这仍然会在我们决定的任何接口/基类/等和Radio类之间创build一个耦合。 基本上每当你改变无线电,信号或继电器类之一,你必须考虑如何可能影响其他两个类。

现在我们尝试一些不同的东西。 我们来创build一个名为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); } } } 

现在我们有一个我们知道的模式 ,我们可以使用它来处理任何数量和types的类,只要它们:

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

所以我们把Radio类改成最终的简单forms:

 class Radio { function ReceiveSignal(signal) { RadioMast.Broadcast("specialradiosignal", signal); } } 

我们将扬声器和继电器添加到RadioMast的接收器列表中,以获得这种types的信号:

 RadioMast.RegisterReceivers("specialradiosignal", speakers.PlaySignal); RadioMast.RegisterReceivers("specialradiosignal", relay.RelaySignal); 

现在讲话人和中继类除了有一个可以接收信号的方法之外,对于任何事情都知之甚less,而作为发布者的Radio类意识到它发布信号的RadioMast。 这是使用诸如发布/订阅之类的消息传递系统的要点。

其他答案在显示模式的工作方面做了很多工作。 我想解决一个隐含的问题, 那就是我最近一直在使用这种模式,而且我发现它涉及到我思考的转变。

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

发送命令和build议刚才发生的事情是有区别的。 考虑到这一点,采取您的原始版本的酒吧/子模式,现在忽略处理程序:

 $.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')); }); 

在用户操作(一个点击)和系统响应(一个命令被删除)之间,已经有一个隐含的强大耦合。 在你的例子中,这个动作就是给出一个命令。 考虑这个版本:

 $.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')); }); 

现在,处理程序正在响应已发生的某些兴趣,但没有义务取消一个命令。 事实上,处理程序可以做各种与删除订单没有直接关系的事情,但仍然可能与调用行为有关。 例如:

 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... } 

一个命令和一个通知之间的区别是一个有用的区别,使用这种模式,国际海事组织。

因此,您不必硬编码方法/函数调用,只需发布​​该事件,而不必关心谁在监听。 这使得发布者独立于订阅者,减less了应用程序的两个不同部分之间的依赖关系(或耦合,无论你喜欢什么术语)。

下面是维基百科提到的耦合的一些缺点

紧密耦合的系统往往performance出以下发展特征,这常常被视为缺点:

  1. 一个模块的改变通常会强迫其他模块的变化产生连锁反应。
  2. 由于增加的模块间依赖性,模块的组装可能需要更多的努力和/或时间。
  3. 一个特定的模块可能难以重用和/或testing,因为依赖模块必须包括在内。

考虑一下封装业务数据的对象。 它有硬编码的方法调用来更新页面,每当年龄设置:

 var person = { name: "John", age: 23, setAge: function( age ) { this.age = age; showAge( age ); } }; //Different module function showAge( age ) { $("#age").text( age ); } 

现在我不能在没有包含showAge函数的情况下testingperson对象。 另外,如果我还需要在其他一些GUI模块中显示年龄,我需要在.setAge对该方法调用进行硬编码,现在在person对象中存在两个不相关模块的依赖关系。 当您看到这些电话正在进行,而且甚至不在同一个文件中时,也很难保持。

请注意,在同一个模块中,您当然可以直接调用方法。 但是商业数据和表面gui行为不应该以任何合理的标准存在于同一个模块中。

PubSub的实现常见于哪里 –

  1. 有一个类似于实现的portlet,其中有多个portlet与事件总线的帮助进行通信。 这有助于在asynchronous架构中创build。
  2. 在一个严格耦合的系统中,pubsub是一个帮助各个模块之间进行通信的机制。

示例代码 –

 var pubSub = {}; (function(q) { var messages = []; q.subscribe = function(message, fn) { if (!messages[message]) { messages[message] = []; } messages[message].push(fn); } q.publish = function(message) { /* fetch all the subscribers and execute*/ if (!messages[message]) { return false; } else { for (var message in messages) { for (var idx = 0; idx < messages[message].length; idx++) { if (messages[message][idx]) messages[message][idx](); } } } } })(pubSub); pubSub.subscribe("event-A", function() { console.log('this is A'); }); pubSub.subscribe("event-A", function() { console.log('booyeah A'); }); pubSub.publish("A"); //executes the methods.