原文: https://howtodoinjava.com/javascript/implement-mvc-and-pubsub-in-javascript/

我们知道什么是 MVC? MVC 代表模型 - 视图 - 控制器。 简而言之,MVC 是一种设计技术,其中将应用组件分为 3 组,以便可以独立开发它们而无需考虑它们将如何交互。 如果构建正确,则很少有配置代码可以绑定它们,并且可以立即使用。

PubSub(发布者订阅者) 模型是设计范式,其中多个订阅者正在监听源上的更改事件,并且一旦发生任何更改,便会立即通知监听器。 在用户交互影响屏幕上多个部分的大型系统中,此模式消除了许多硬编码,并提供了设计灵活性。

在 JavaScript 中实现 MVC 和 PubSub - 图1

JavaScript 中的 PubSub + MVC

在本教程中,我们将学习以下概念:

  1. Building Model-View-Controller components
  2. Building Publisher Subscriber infrastructure
  3. Understanding Event Notification mechanism
  4. Demo application

让我们从构建 MVC 组件开始。

构建模型视图控制器组件

在 JavaScript 中,如果必须开发 MVC 结构,则至少需要编写 3 个对象。 我只花 3 个使例子更加关注概念。

例如,我以媒体播放器为例。 此媒体播放器附有一个播放列表,用户可以使用按键事件在此播放列表上向前和向后移动。

模型:存储当前视图状态

playlist – 数组对象将所有曲目存储在当前可用的播放列表中。

currentIndex – 当前播放的曲目

模型还包含帮助用户在用户交互后保持其当前状态更改的函数。

  1. var Model = {
  2. playlist: new Array(),
  3. currentIndex : 0,
  4. reLoad: function() {
  5. currentIndex = 0;
  6. var tracks = document.getElementById("playListSelector").options;
  7. for(var i=0; i<tracks.length; i++)
  8. {
  9. this.playlist&#91;i&#93; = tracks&#91;i&#93;.value;
  10. }
  11. },
  12. next: function () {
  13. if(this.currentIndex < (this.playlist.length-1))
  14. this.currentIndex++;
  15. publish(this);
  16. },
  17. prev: function () {
  18. if(this.currentIndex > 0)
  19. this.currentIndex--;
  20. publish(this);
  21. },
  22. current: function () {
  23. publish(this);
  24. }
  25. };

视图:表示用户与之交互的屏幕

该对象只有一种方法可以在屏幕上呈现用户事件的结果。

  1. var View = {
  2. notify: function(model) {
  3. document.getElementById("playListSelector").selectedIndex = model.currentIndex;
  4. }
  5. };

控制器:视图调用控制器以更改模型

控制器具有在用户交互期间将被调用的函数。

  1. var Controller = {
  2. model: Model,
  3. moveNext: function () {
  4. this.model.next();
  5. return this;
  6. },
  7. movePrev: function () {
  8. this.model.prev();
  9. return this;
  10. },
  11. getCurrent: function () {
  12. this.model.current();
  13. return this;
  14. }
  15. };

构建发布者订阅服务器基础结构

到目前为止,一切都很好。 现在,我们将添加一些 pub-sub 逻辑,以便无论何时触发任何用户事件,都会通知所有已注册的视图,并且它们可以进行所需的视觉更改。

  1. //All subscribers for a event
  2. var subscribers = [];
  3. function publish(event) {
  4. for (i in subscribers) {
  5. subscribers[i].notify(event);
  6. }
  7. };

上面的代码声明了一个数组,该数组可用于存储所有感兴趣的视图以将其自身注册为事件监听器。 每当任何事件作为用户交互触发时,都会通知他们该事件。

要将视图注册为事件监听器,将使用以下代码:

  1. //Subscribe for updates
  2. subscribers.push(View);

了解事件通知机制

事件处理按以下顺序执行:

视图触发事件 -> 控制器触发模型更新 -> 模型将通知发送到 pubsub -> pubsub 通知所有有关事件的视图,以便它们可以更新用户屏幕

在上面的代码段中,假设用户按下了播放列表中的下一首曲目。 这是控制流:

  1. 用户按下“下一首”按钮
  2. 控制器的moveNext()方法调用
  3. moveNext()触发模型的next()方法
  4. next()方法增加当前正在播放曲目的currentIndex
  5. next()方法使用publish()方法发布事件
  6. publish()方法调用notify()方法是所有注册的订户
  7. 视图notify()方法根据模型的当前状态更新用户屏幕

这样,所有可能的事件都将从控制器处理到视图层。 最后,我们一直都处于模型的当前状态。

演示应用

我已经在一个文件中使用了上述所有代码段,并使用 HTML select元素进行了虚拟播放列表行为。 select的当前选定选项代表媒体播放器中当前播放的曲目。

让我们看一下完整的演示代码:

  1. <html>
  2. <head>
  3. <meta charset="utf-8">
  4. <script language="javascript">
  5. // PubSub
  6. var subscribers = [];
  7. function publish(event) {
  8. for (i in subscribers) {
  9. subscribers[i].notify(event);
  10. }
  11. };
  12. // MVC
  13. var Model = {
  14. playlist: new Array(),
  15. currentIndex : 0,
  16. reLoad: function() {
  17. currentIndex = 0;
  18. var tracks = document.getElementById("playListSelector").options;
  19. for(var i=0; i<tracks.length; i++)
  20. {
  21. this.playlist&#91;i&#93; = tracks&#91;i&#93;.value;
  22. }
  23. },
  24. next: function () {
  25. if(this.currentIndex < (this.playlist.length-1))
  26. this.currentIndex++;
  27. publish(this);
  28. },
  29. prev: function () {
  30. if(this.currentIndex > 0)
  31. this.currentIndex--;
  32. publish(this);
  33. },
  34. current: function () {
  35. publish(this);
  36. }
  37. };
  38. var View = {
  39. notify: function(model) {
  40. document.getElementById("output").innerHTML = JSON.stringify(model);
  41. document.getElementById("playListSelector").selectedIndex = model.currentIndex;
  42. }
  43. };
  44. var Controller = {
  45. model: Model,
  46. moveNext: function () {
  47. this.model.next();
  48. return this;
  49. },
  50. movePrev: function () {
  51. this.model.prev();
  52. return this;
  53. },
  54. getCurrent: function () {
  55. this.model.current();
  56. return this;
  57. }
  58. };
  59. subscribers.push(View); // Subscribe for updates
  60. function initializeModel()
  61. {
  62. Model.reLoad();
  63. }
  64. </script>
  65. </head>
  66. <body onload="initializeModel()">
  67. <input type="button" onclick="Controller.getCurrent();" value="Current Track">
  68. <input type="button" onclick="Controller.moveNext();" value="Next Track">
  69. <input type="button" onclick="Controller.movePrev();" value="Previous Track">
  70. <select id="playListSelector" multiple readonly>
  71. <option value="0">Track 1</option>
  72. <option value="1">Track 2</option>
  73. <option value="2">Track 3</option>
  74. <option value="3">Track 4</option>
  75. </select>
  76. <span id="output" />
  77. </body>
  78. </html>

上面的代码还有另外一个方法initializeModel(),该方法用于在页面加载时使用播放列表项初始化模型对象。 现在,当我们按“下一个曲目”时,选择元素中的下一个选项被选中。 同样,按下“上一曲目”按钮,则在选择列表中选择了上一个选项。

您将看到如下运行代码:

在 JavaScript 中实现 MVC 和 PubSub - 图2

JavaScript 中的 MVC + PubSub 的示例界面

如果不清楚或您有任何建议/查询,请发表评论。

————————————————————————————————————

更新:

经过简短的邮件讨论后,Brook Monroe 向我发送了类似示例的更好的代码示例。 尽管本教程的目的不是更好的代码实践,而是详细介绍了概念。 我在下面共享更新的代码以供参考。 它可能会帮助您。

  1. <html>
  2. <head>
  3. <meta charset="utf-8">
  4. <script src="./pubsub.js"></script>
  5. </head>
  6. <body>
  7. <button id="btnCurrent">Current Track</button>
  8. <button id="btnNext">Next Track</button>
  9. <button id="btnPrev">Previous Track</button>
  10. <select id="playListSelector" multiple readonly>
  11. <option value="0" selected>Track 1</option>
  12. <option value="1">Track 2</option>
  13. <option value="2">Track 3</option>
  14. <option value="3">Track 4</option>
  15. </select>
  16. <span id="output"></span>
  17. </body>
  18. </html>
  19. //pubsub.js
  20. // PubSub
  21. ( function () {
  22. "use strict";
  23. var subscribers = [],
  24. elCache = {},
  25. Model = {
  26. playlist : [],
  27. currentIndex : 0,
  28. reLoad : function()
  29. {
  30. var tracks = Array.prototype.slice.call(elCache.get("playListSelector").options);
  31. this.playlist = [];
  32. tracks.forEach( function (e,i) { this.playlist.push(tracks[i].value); }, Model);
  33. this.currentIndex = 0;
  34. },
  35. next : function ()
  36. {
  37. if (this.currentIndex < (this.playlist.length-1)) {
  38. this.currentIndex++;
  39. }
  40. subscribers.publish(this);
  41. },
  42. prev : function ()
  43. {
  44. if (this.currentIndex > 0) {
  45. this.currentIndex--;
  46. }
  47. subscribers.publish(this);
  48. },
  49. current : function ()
  50. {
  51. subscribers.publish(this);
  52. }
  53. },
  54. // MVC
  55. View = {
  56. notify : function(model)
  57. {
  58. elCache.get("output").innerHTML = JSON.stringify(model);
  59. elCache.get("playListSelector").selectedIndex = model.currentIndex;
  60. }
  61. },
  62. Controller = {
  63. moveNext: function ()
  64. {
  65. Model.next();
  66. return this;
  67. },
  68. movePrev: function ()
  69. {
  70. Model.prev();
  71. return this;
  72. },
  73. getCurrent: function ()
  74. {
  75. Model.current();
  76. return this;
  77. }
  78. };
  79. function start()
  80. {
  81. elCache.get = function (elId)
  82. {
  83. return this[elId] || ( this[elId] = document.getElementById(elId) );
  84. };
  85. subscribers.publish = function (event)
  86. {
  87. this.forEach( function (e) { e.notify(event); } );
  88. };
  89. subscribers.push(View); // Subscribe for updates
  90. elCache.get("btnCurrent").addEventListener("click", Controller.getCurrent.bind(Model));
  91. elCache.get("btnNext").addEventListener("click", Controller.moveNext.bind(Model));
  92. elCache.get("btnPrev").addEventListener("click", Controller.movePrev.bind(Model));
  93. Model.reLoad.bind(Model)();
  94. }
  95. window.addEventListener("load",start,false);
  96. } )();

祝您学习愉快!