105

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

4

7 回答 7

228

这都是关于松散耦合和单一职责的,这与过去几年非常现代的 JavaScript 中的 MV* (MVC/MVP/MVVM) 模式密切相关。

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

在谈到松耦合时,我们应该提到关注点分离. 如果您正在使用 MV* 架构模式构建应用程序,那么您总是有一个模型和一个视图。模型是应用程序的业务部分。您可以在不同的应用程序中重用它,因此将它与要显示它的单个应用程序的视图结合起来并不是一个好主意,因为通常在不同的应用程序中您有不同的视图。因此,使用发布/订阅进行模型视图通信是一个好主意。当您的模型发生更改时,它会发布一个事件,视图会捕获它并自行更新。您没有任何来自发布/订阅的开销,它可以帮助您解耦。以同样的方式,您可以将应用程序逻辑保留在控制器中(例如,MVVM、MVP,它不完全是控制器),并使视图尽可能简单。当你的 View 改变(或者用户点击某个东西,例如)它只是发布一个新事件,Controller 捕获它并决定做什么。如果你熟悉MVC模式或Microsoft 技术(WPF/Silverlight)中的MVVM ,您可以将发布/订阅视为观察者模式。这种方法用于 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 代码,它只是我的一个示例):

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出版的。

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

于 2012-11-22T13:38:33.317 回答
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 类中的一个列表,并且总是可以在我们想要收听信号的任何对象上调用相同的函数。但这仍然会在我们决定的任何接口/基类/等与 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);
        }
    }
}

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

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

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

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

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

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

现在 Speakers 和 Relay 类除了有一个可以接收信号的方法外,对任何事情的了解都是零,而作为发布者的 Radio 类知道它向其发布信号的 RadioMast。这就是使用像发布/订阅这样的消息传递系统的重点。

于 2012-11-22T13:29:46.517 回答
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。

于 2015-09-24T21:06:25.600 回答
4

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

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

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

  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现在,如果不包含该功能,我将无法测试 person 对象。此外,如果我还需要在其他一些 GUI 模块中显示年龄,我需要对该方法调用进行硬编码, .setAge现在 person 对象中存在 2 个不相关模块的依赖关系。当你看到这些调用被调用并且它们甚至不在同一个文件中时,它也很难维护。

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

于 2012-11-22T12:50:21.027 回答
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.
于 2017-04-23T17:41:20.403 回答
1

论文“发布/订阅的许多方面”是一本很好的读物,他们强调的一件事是在三个“维度”上解耦。这是我的粗略总结,但也请参考论文。

  1. 空间解耦。交互的各方不需要相互认识。发布者不知道谁在听,有多少人在听,或者他们在做什么。订阅者不知道是谁在生产这些事件,有多少生产者等等。
  2. 时间脱钩。在交互过程中,交互方不需要同时处于活动状态。例如,当发布者发布一些事件时,订阅者可能会断开连接,但它可以在它上线时做出反应。
  3. 同步解耦。发布者在生成事件时不会被阻塞,订阅者可以在订阅的事件到达时通过回调异步通知。
于 2018-03-30T19:09:16.447 回答
0

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

Javascript 没有为代码对象提供任何机制来创建自己的事件。所以你需要一种事件机制。发布/订阅模式将满足这一需求,您可以选择最适合您自己需求的机制。

现在我们可以看到对 pub/sub 模式的需求,那么您是否愿意以不同于处理 pub/sub 事件的方式来处理 DOM 事件?为了降低复杂性和其他概念,例如关注点分离 (SoC),您可能会看到一切统一的好处。

因此自相矛盾的是,更多的代码可以更好地分离关注点,这可以很好地扩展到非常复杂的网页。

我希望有人认为这是一个足够好的讨论,而不需要详细说明。

于 2018-07-24T07:27:40.857 回答