[发布订阅模式 --- Publish/Subscribe]

1,994 阅读3分钟

发布订阅模式 Publish/Subscribe

事件驱动的事实

发布订阅模式 Publish/Subscribe 非常适用于 JavaScript 生态系统,主要原因是 ECMAScript 是事件驱动的,在浏览器环境和Node.js环境,小程序环境,等等。。。 DOM 操作是作为脚本编程的主要交互 API。

图解

img

观察者模式 Observer 的本质

观察者模式的一个特定的要求: 需要接收到主题通知的观察者(或对象)必须订阅改变内容的事件。

发布订阅模式

首先要明白一个重要的点就是: 发布订阅模式(Publish/Subscribe),含有一个中间层,不是发布直接触发事件更新订阅者的内容。发布订阅模式是通过使用主题/事件通道(Topic/Event Channel)这个中介 --- 接收通知的对象和激活事件对象。这个事件系统允许代码定义应用程序的特定事件,这些事件可以传递自定义参数,自定义参数包含订阅者所需的值。

解耦

由于 Observer 观察者和主题是有对应关系的,虽然没有强耦合,但是还是产生了依赖的关系。在发布订阅中,就是避免 订阅者和发布者产生依赖关系,所以由中介来处理这些事务。与 Observer 不同的另一个点,它允许任何的订阅者执行适当的事件处理程序注册接收发布者的通知。

优点

  • 解耦带来的优点在JavaScript中是很好的的解决方案,可以更好的将代码分解成不同的块。改进代码的管理和潜在的问题。

  • 带来了很大的灵活性,也就是在观察者和目标Subject直接存在动态的关系

缺点

缺点也来源于它们的优点, 在发布订阅模式中,通过从订阅中解耦发布者,两者没有了依赖关系,我们很难真正的保证应用程序的特定部分按照我们期望的进行。

例如: 订阅者的代码运行出现了问题,这意味发布者是不知道的。同时由于是解耦的发布者更换了发布者,订阅者也是不能知晓的。

简单的代码实现

var mailCount = 0var subs1 = subscribe('inbox/newMessage', function (topic, data) {
  console.log('A new message was received: ', topic)
  $('.messageSender').html(data.sender)
  $('.messagePreview').html(data.body)
})

var subs2 = subscribe('inbox/newMessage', function (topic, data) {
  $('.newMessageCounter').html(mailCount++)
})

publish('inbox/newMessage', [{
  sender: 'hello@google.com',
  body: 'Hey, there! How are your doing today?'
}])

subscribe(subs1)
subscribe(subs2)

库类中的发布订阅

  • jQuery
  • Dojo
  • YUI
  • Vue EventBus
  • AmplifyJS
  • Radio.js
  • PubSubJS
  • PureJS PubSub
  • pubsubz

实现一个简单 PubSub

var pubsub = {};

  (function(q) {
    var topics = {},
      subUid = -1;

    q.publish = function(topic, args) {
      if (!topics[topic]) {
        return false
      }
      var subscribes = topics[topic],
        len = subscribes ? subscribes.length : 0

      while (len--) {
        subscribes[len].func(topic, args)
      }
      return this
    }

    q.subscribe = function(topic, func) {
      if (!topics[topic]) {
        topics[topic] = []
      }

      var token = (++subUid).toString()
      topics[topic].push({
        token: token,
        func: func,
      })
      return token
    }

    q.unsubscribe = function(token) {
      for (var m in topics) {
        if (topics[m]) {
          for (var i = 0, j = topics[m].length; i < j; i++) {
            if (topics[m][i].token === token) {
              topics[m].splice(i, 1)
              return token
            }
          }
        }
      }
      return this
    }
  }(pubsub))

实例1 --- 一个简单的消息处理程序

var messageLogger = function(topics, data) {
  console.log(`Logging: ${topics}: ${data}`)
}

var subscription = pubsub.subscribe('inbox/newMessage', messageLogger)

pubsub.publish('inbox/newMessage', 'Hello world!')

pubsub.publish('inbox/newMessage', ['test', 'a', 'b', 'c'])

pubsub.publish('inbox/newMessage', {
  sender: 'hello@google.com',
  body: 'Hey, again!',
})
pubsub.unsubscribe(subscription)

pubsub.publish('inbox/newMessage', 'Hello! are you still there?')

实例2 --- 用户界面通知

// newDateAvaildate topic

var getCurrentTime = function () {
  var date = new Date(),
    m = date.getMonth() + 1,
    d = date.getDate(),
    y = date.getFullYear(),
    t = date.toLocaleTimeString().toLowerCase();
    
  return (`${m}/${d}/${y} ${t}`)
}

var grid = {
  addGridRow: function addGridRow(data) {
    console.log('update grid component with:' + data)
  },
  updateCounter: function updateCounter(data) {
    console.log("data last updated at:" + getCurrentTime() + " with" + data)
  }
}

var gridUpdate = function (topic, data) {
  if (data !== 'undefined') {
    grid.addGridRow(data)
    grid.updateCounter(data)
  }
}

// eslint-disable-next-line no-unused-vars
var subcriber = pubsub.subscribe('newDateAvaildate', gridUpdate)

pubsub.publish("newDateAvaildate", {
  summary: "Apple made $5 billion",
  identifier: 'APPL',
  stockPrice: 570.91
})

pubsub.publish("newDateAvaildate", {
  summary: "Microsoft made $20 million",
  identifier: 'MSFT',
  stockPrice: 30.85
})

实例三 --- 一个简单的影评

<script id="userTemplate" type="text/html">
  <li><%= name %></li>
</script>

<script id="ratingsTemplate" type="text/html">
  <li><strong><%= title %></strong> was rating<%= rating %>/5</li>
</script>

这里使用的 Underscore 的 template 方法来处模板

;(function($) {
  $.subscribe("/new/user", function (e, data) {
    var compiledTemplate;

    if(data) {
      compiledTemplate = _.template($('#ratingsTemplate').html());
      $('#users').append(compiledTemplate(data))
    }
  })

  $.subscribe("/new/rating", function (e, data) {
    var compiledTemplate;

    if(data) {
      compiledTemplate = _.template($('#userTemplate').html());
      $('#ratings').append(compiledTemplate(data))
    }
  })

  $('#add').on("click", function (e) {
    e.preventDefault();

    var strUser = $("#twitter_handle").val();
    var strMovie = $("#moive_seen").val();
    var strRating = $("#moive_rating").val();

    $.subscribe("/new/user",{name: strUser})
    $.subscribe("/new/rating",{title: strMovie, rating: strRating})
  })
}(jQuery))

实例4 --- ajax 请求

<form id="flickSearch">
  <input type="text" name="tag" id="query" />
  // some codes
</form>
<script id="resultTemplate" type="text/html">
  <% _.each(items, function(item) {)%>
    <li><p><img src="<%=  item.media.m %>"/></p></li>
  <% });%>
</script>
;(function($) {
  var resultTemplate = _.template($("#resultTemplate").html());


  $.subscribe("/search/tags", function (e, tags) {
    $("#searchResult").html("Searched for: " + tags + "")
  })

  $.subscribe("/search/resultSet", function (e, results) {
    $('#searchResults').append(resultTemplate(results))
    $('#searchResults').append(compiled_template(results))
  })

  $('#add').on("click", function (e) {
    e.preventDefault();

    var strUser = $("#twitter_handle").val();
    var strMovie = $("#moive_seen").val();
    var strRating = $("#moive_rating").val();

    $.subscribe("/new/user",{name: strUser})
    $.subscribe("/new/rating",{title: strMovie, rating: strRating})
  })
  $('#flickrSearch').submit(function (e) {
    e.preventDefault()
    var tags = $(this).find("query").val()

    if(!tags) {
      return
    }

    $.publiah('/search/tags', [$.trim(tags)])
  })

  $.subscribe("/search/tags", function (e, tags) {
    $.getJSON('address', {
      // some params
    }, function (data) {
      if(data) {
        if(!data.items.length) {
          return
        }
        $.publish("/search/resultSet", data.items)
      }
    })
  })
}(jQuery))