为什么要使用Publish / Subscribe模式(在JS / jQuery中)?


103

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

因为它引入了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'));
});

我已经确定地了解了这种模式,但是我无法想象为什么这将是必要的。我看到的教程解释了如何实现这种模式,仅涵盖了我自己的基本示例。

我认为pub / sub的有用性将使其在更复杂的应用程序中显而易见,但我无法想象。恐怕我完全不明白这一点。但我想知道要点是什么!

您能否简要说明这种模式在什么情况下以及在什么情况下是有利的?像上面的示例一样,使用pub / sub模式编写代码片段是否值得?

Answers:


222

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

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

在谈论松散耦合时,我们应该提到关注点分离。如果要使用MV *架构模式构建应用程序,则始终会有一个模型和一个视图。模型是应用程序的业务部分。您可以在不同的应用程序中重用它,因此,将它与要显示它的单个应用程序的视图耦合不是一个好主意,因为通常在不同的应用程序中您具有不同的视图。因此,使用发布/订阅进行Model-View通信是一个好主意。当您的模型更改时,它会发布一个事件,视图会捕获该事件并自行更新。您没有发布/订阅的任何开销,它可以帮助您实现解耦。以相同的方式,您可以将应用程序逻辑保留在Controller中(例如,MVVM,MVP并非完全是Controller),并尽可能简化View。当您的视图发生更改(例如,用户单击某项)时,它只是发布一个新事件,控制器会捕获该事件并决定要执行的操作。如果您熟悉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 *方法,则可以使用一些不同的方法(我将在下面描述的方法和最后提到的方法之间有一个交叉点)。只需在不同模块中构建应用程序即可。例如看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出版的。

缺点:您必须注意过度使用发布/订阅。如果您有数百个事件,则对所有事件进行管理都会变得非常混乱。如果未使用命名空间(或未正确使用命名空间),则也可能会发生冲突。可以在https://github.com/ajacksified/Mediator.js上找到Mediator的高级实现,该实现看起来很像发布/订阅。它具有命名空间和事件“冒泡”之类的功能,这些功能当然可以中断。发布/订阅的另一个缺点是很难进行单元测试,可能难以隔离模块中的不同功能并进行独立测试。


3
谢谢,这很有意义。我一直在PHP中使用MVC模式,因此我对它很熟悉,但是我从事件驱动的编程角度就没有考虑过。:)
Maccath 2012年

2
感谢您的描述。确实帮助了我解决这个问题。
flybear 2015年

1
那是一个很好的答案。无法阻止我自己投票:)
Naveed Butt 2015年

1
很棒的解释,很多例子,进一步的阅读建议。A ++。
卡森2015年

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

每当无线电接收到信号时,我们都希望许多中继以某种方式中继消息。继电器的数量和类型可以不同。我们可以这样做:

class Radio {
    var relayList = [];

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

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

}

这很好。但是现在想象一下,我们希望一个不同的组件也可以接收Radio类接收的信号,即Speakers:

(很抱歉,如果类比不是一流的...)

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

    }

}

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

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

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

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

因此,我们将Radio类更改为其最终的简单形式:

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

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

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

现在,扬声器和中继类对任何事物都一无所知,除了它们具有可以接收信号的方法外,作为发布者的Radio类也知道向其发布信号的RadioMast。这就是使用诸如发布/订阅之类的消息传递系统的重点。


举一个具体的例子来展示实现pub / sub模式比使用“常规”方法更好的例子真是太好了!谢谢!
Maccath 2012年

1
别客气!就我个人而言,我经常发现,在涉及到新的模式/方法时,我的大脑不会“点击”,直到我意识到它可以为我解决的实际问题。sub / pub模式非常适合在概念上紧密耦合的体系结构,但我们仍希望将它们尽可能地分开。想象一下,一个游戏,你有上百个对象,所有必须的事情他们周围发生的事情,例如反应,而这些对象可以是一切:播放器,子弹,树,几何,图形用户界面等等等等
安德斯ARPI

3
JavaScript没有class关键字。请强调这个事实,例如。通过将您的代码分类为伪代码。
罗布W

实际上,在ES6中有一个class关键字。
Minko Gechev 2012年

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

在用户操作(单击)和系统响应(已删除订单)之间已经存在隐含的强耦合。在您的示例中,操作有效地发出了命令。考虑以下版本:

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

命令和通知之间的区别是使用此模式IMO的有用区别。


如果您的最后2个功能(remindUserToFlossincreaseProgrammerBrowniePoints)位于不同的模块中,您会在其中一个接一个地发布两个事件,handleRemoveOrderRequest还是在完成后flossModule向一个browniePoints模块发布事件remindUserToFloss()
Bryan P

4

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

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

紧密耦合的系统往往表现出以下发展特征,这些特征通常被视为劣势:

  1. 一个模块的更改通常会导致其他模块的更改产生连锁反应。
  2. 由于模块间依赖性增加,模块的组装可能需要更多的精力和/或时间。
  3. 特定模块可能更难以重用和/或测试,因为必须包含相关模块。

考虑类似封装业务数据的对象之类的东西。它具有硬编码的方法调用,可以在设置年龄时更新页面:

var person = {
    name: "John",
    age: 23,

    setAge: function( age ) {
        this.age = age;
        showAge( age );
    }
};

//Different module

function showAge( age ) {
    $("#age").text( age );
}

现在,如果不包含showAge功能,就无法测试人员对象。另外,如果还需要在其他GUI模块中显示年龄,则需要在中硬编码该方法调用 .setAge,现在person对象中有2个无关模块的依赖项。当您看到正在进行的调用而且它们不在同一文件中时,也很难维护。

注意,在同一模块内,您当然可以直接调用方法。但是,按照任何合理的标准,业务数据和表面的gui行为都不应位于同一模块中。


我在这里不理解“依赖”的概念。在我的第二个示例中,依赖关系在哪里?在我的第三个示例中,依赖关系在哪里缺失?我看不到我的第二个片段和第三个片段之间的任何实际区别-似乎只是在没有真正原因的情况下在函数和事件之间添加了一个新的“层”。我可能是瞎子,但我认为我需要更多的指导。:(
Maccath 2012年

1
您能否提供一个示例用例,其中发布/订阅比仅使一个函数执行相同的功能更合适?
杰弗里·斯威尼

@Maccath简而言之:在第三个示例中,您甚至不知道甚至不必知道它是否 removeOrder存在,因此您不能依赖它。在第二个示例中,您必须知道。
Esailija

虽然我仍然觉得有更好的方法来处理您在此处描述的内容,但我至少确信这种方法是有目的的,尤其是在具有许多其他开发人员的环境中。+1
Jeffrey Sweeney 2012年

1
@Esailija-谢谢,我想我了解得更好。所以...如果我完全删除订户,它不会出错或什么,它什么都不做?并且您说这在您要执行操作的情况下可能有用,但不一定在发布时知道哪个功能最相关,但是订阅者可能会根据其他因素进行更改?
Maccath 2012年

1

在以下地方通常可以看到PubSub实现-

  1. 有一个类似于portlet的实现,其中有多个portlet在事件总线的帮助下进行通信。这有助于创建aync体系结构。
  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("event-A"); //executes the methods.

1

论文“发布/订阅的许多面孔”是一本好书,他们强调的一件事是将三个“维度”分离。这是我的简要摘要,但也请参考本文。

  1. 空间解耦。交互各方不需要彼此了解。发布者不知道谁在听,有多少在听,或者他们正在处理事件。订阅者不知道谁在制作这些事件,有多少制作人,等等。
  2. 时间解耦。交互过程中,交互方不需要同时处于活动状态。例如,在发布者发布某些事件时,订阅者可能会断开连接,但是当它联机时,它可以对此做出反应。
  3. 同步解耦。发布者在生成事件时不会被阻止,只要订阅者订阅的事件到达,就可以通过回调异步通知订阅者。

0

简单答案 最初的问题是寻找简单答案。这是我的尝试。

Javascript不提供任何机制来使代码对象创建自己的事件。因此,您需要一种事件机制。“发布/订阅”模式将满足此需求,您可以选择最适合自己需求的机制。

现在我们可以看到需要使用pub / sub模式,那么您宁愿要以与处理pub / sub事件不同的方式来处理DOM事件吗?为了降低复杂度,以及减少关注点分离(SoC)之类的其他概念,您可能会看到统一的好处。

因此,矛盾的是,更多的代码创建了更好的关注点分离,从而可以很好地扩展到非常复杂的网页。

我希望有人能在不进行详细讨论的情况下找到足够好的讨论。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.