MVC:控制器controller和狀態(tài)切換(未完成)

主題:如何使用控制器模式在客戶端保持一個(gè)狀態(tài)

包括以下分支:

1.如何將邏輯封裝成模塊,阻止全局命名空間的污染
2.如何使用視圖來進(jìn)一步簡(jiǎn)化控制器的結(jié)構(gòu),以及怎樣在視圖中實(shí)現(xiàn)DOM事件監(jiān)聽
3.路由怎么選擇,包括使用URL中的hash片段,使用新的HTML5 History API等技術(shù),以及確保解釋兩種方法的利弊


1.如何將邏輯封裝成模塊,阻止全局命名空間的污染

其實(shí)就是使用自執(zhí)行匿名函數(shù)。

相關(guān)鏈接 Self-Executing Anonymous Functions

例子:

(function(){
  console.log('Hello World!');
})();

根據(jù)鏈接里的文章,自執(zhí)行匿名函數(shù)可以全局導(dǎo)入,即將全局對(duì)象作為參數(shù)傳入。

(function ($) {
    / ..... / 
}) (jQuery);

也可全局導(dǎo)出,有兩種方式:

第一種,傳入window全局對(duì)象

(function ($, exports) {
    / ..... 
    exports.xxx = xxx;
    / 
}) (jQuery, window);

第二種,用一個(gè)全局對(duì)象的this

var exports = this;   // exports被賦值全局的this

(function ($) {
    / ..... 
    var xxx = {};
    xxx.create = function () {
        / .... / 
    };
    exports.xxx = xxx;
    / 
}) (jQuery);

小結(jié):通過使用自執(zhí)行匿名函數(shù)可以達(dá)到模塊化的目的


2.如何使用視圖來進(jìn)一步簡(jiǎn)化控制器的結(jié)構(gòu),以及怎樣在視圖中實(shí)現(xiàn)DOM事件監(jiān)聽

為了實(shí)現(xiàn)事件回調(diào)函數(shù),需要處理上下文問題。上下文問題是指在JS里每次創(chuàng)建函數(shù),這個(gè)函數(shù)的引用都是window,即this指向的window全局,而嵌套的事件函數(shù)需要操作的卻又是上級(jí)函數(shù)的引用,即上級(jí)函數(shù)的this,這當(dāng)然就引起了矛盾。

比如:

(function () {
   assertEqual(this, window); //  相等,即函數(shù)的this = window
}) ();

所以需要處理上下文,即處理事件函數(shù)的this指向。如果想要自定義作用域的上下文,需要將函數(shù)寫入一個(gè)對(duì)象中,比如:

(function () {
    var mod = {};
    
    mod.xxx = function () {
        / ... /
    };
}) ();

這樣xxx的作用域,即this就指向的mod對(duì)象。

然后怎么用視圖來簡(jiǎn)化控制器的結(jié)構(gòu)能?

這里使用全局this,而不是傳入window,來抽象出控制器庫,方便控制器的復(fù)用。

var exports = this;

(function ($) {
    var mod = {};
    
    mod.create = function (includes) {
        var result = function () {
            this.init.apply(this, arguments);
        };
 
        result.fn = result.prototype;
        result.fn.init = function() {};
        
        result.proxy = function(func) { return $.proxy(func, this); );
        result.fn.proxy = result.proxy;

        result.include = function(ob) { $.extend(this.fn, ob); };
        result.extend = function(ob) { $.extend(this, ob); };
        if (includes) {
            result.include(includes);
        };

        exports.Controller = mod;
    };
}) ( jQuery ) ;

補(bǔ)上用window的寫法:

      (function ($, exports) {
        var mod = {};

        mod.create = function (includes) {
          var result = function () {
            this.init.apply(this, arguments);
          };

          result.fn = result.prototype;
          result.fn.init = function () {};

          result.proxy = function (func) {
            return $.proxy(func, this);
          };
          result.fn.proxy = result.proxy;
          result.include = function (ob) {
            $.extend(this.fn, ob);
          };
          result.extend = function (ob) {
            $.extend(this, ob);
          };
          if (includes) {
            result.include(includes)
          };

          return result;
        };

        exports.Controller = mod;
      })(jQuery, window);

下面則是用控制器庫的API:Controller.create()來創(chuàng)建并實(shí)例化每個(gè)具體對(duì)應(yīng)視圖元素的控制器。

這里注意用jQuery.ready()的簡(jiǎn)寫jQuery(function($) {...})來確??刂破魇窃贒OM渲染完成后才被加載的。相當(dāng)于window.onload的功能

jQuery(function($) {
  var ToggleView = Controller.create({
    init: function (view) {
      this.view = $(view);
      // 下面兩項(xiàng)是jQuery的事件函數(shù),因此會(huì)給回調(diào)函數(shù)傳入event參數(shù),即e
      this.view.mouseover(this.proxy(this.toggleClass), true); 
      this.view.mouseout(this.proxy(this.toggleClass), false);
    },

    this.toggleClass: function(e) {
      this.view.toggleClass("over", e.data); // 這里是jQuery的事件函數(shù)
    }
  });

  // 實(shí)例化控制器,即調(diào)用上面定義的,被result繼承的init()
  new ToggleView("#view");
});

這里就是一個(gè)視圖對(duì)應(yīng)一個(gè)控制器(具體的代碼體現(xiàn)是最后的new ToggleView并傳入("#view")參數(shù) ),而一個(gè)控制器包含一個(gè)或幾個(gè)相應(yīng)事件,因此也就是一個(gè)視圖對(duì)應(yīng)一個(gè)或幾個(gè)事件。

有一個(gè)好處是,這個(gè)視圖#view綁定了這個(gè)controller,如果后續(xù)要在這個(gè)controller里對(duì)視圖元素查找(可以用$("#view".find("xxx")方法)則限制在#view之下,不會(huì)全DOM都查找一遍,從而提高了查找速度。

再一個(gè)示例,視圖#user的對(duì)應(yīng)控制器:

      var exports = this;

      jQuery(function ($) {
        exports.SearchView = Controller.create({
          elements: {
            "input[type=search]": "searchInput",
            "form": "searchForm"
          },

          init: function (element) {
            this.el = $(element);
            this.refreshElements();
            this.searchForm.submit(this.proxy(this.search));
          },

          search: function (e) {
            alert("Searching: " + this.searchInput.val());
            return false;
          },

          // 私有
          $: function (selector) {
            return $(selector, this.el);
          },

          refreshElements: function () {
            for (var key in this.elements) {
              this[this.elements[key]] = this.$(key);
            }
          }
        });

        new SearchView("#users");
      });

這個(gè)被傳入的是ID (#users),然后用jQuery的選擇器獲?。╰his.el = $(element);)。那么到這都有個(gè)問題,視圖元素、選擇器selector(對(duì)應(yīng)有事件)不多還好,可一旦語義不明顯的選擇器很多就會(huì)顯得很亂。

因此這個(gè)控制器的不一樣在于開辟了一個(gè)空間專門存放選擇器selector到一個(gè)變量的映射表(推薦的寫法),這個(gè)映射表的實(shí)現(xiàn)則是基于示例里私有的兩個(gè)函數(shù)$和refreshElemments。映射表的作用是,在實(shí)例化controller之后,就可以用this.xxx(對(duì)應(yīng)的選擇器變量名)代替選擇器名了,而變量名則可以用語義更清晰的名字,對(duì)代碼閱讀更有幫助,因此可以讓代碼更簡(jiǎn)潔易讀(之后還有對(duì)事件的映射,創(chuàng)建相應(yīng)映射表和映射函數(shù),效果相同)。

tips:注意是選擇器名稱在前,對(duì)應(yīng)的變量名在后,這樣在映射時(shí)才能正確對(duì)應(yīng)(事件映射相同)

(待添加事件映射示例)


狀態(tài)機(jī)

mvc結(jié)合狀態(tài)機(jī)在某一對(duì)象有多種狀態(tài)且經(jīng)常需要轉(zhuǎn)換的時(shí)候,使用狀態(tài)機(jī)實(shí)現(xiàn)非常方便。在model層給對(duì)象添加狀態(tài)機(jī)組件,然后在觸發(fā)某種狀態(tài)時(shí)(onstart,onready,onrun…)分發(fā)事件,然后再view層監(jiān)聽此事件,當(dāng)model處于某種狀態(tài)時(shí),觸發(fā)相應(yīng)的事件,view層監(jiān)聽到事件后做出不同的動(dòng)作。關(guān)于mvc、狀態(tài)機(jī)的使用可以查看sample下的demo

完整示例:狀態(tài)機(jī)完整示例代碼

狀態(tài)機(jī)本質(zhì)上由兩部分組成:狀態(tài)和轉(zhuǎn)換器。

它只有一個(gè)活動(dòng)狀態(tài),也包含很多非活動(dòng)狀態(tài)。當(dāng)活動(dòng)狀態(tài)之間相互切換時(shí)就會(huì)調(diào)用狀態(tài)轉(zhuǎn)換器。

狀態(tài)機(jī)的工作場(chǎng)景:

存在兩個(gè)視圖,他們的存在是互斥關(guān)系,其中一個(gè)顯示時(shí),另一個(gè)就是隱藏的。比如聯(lián)系人列表,一個(gè)視圖用來顯示聯(lián)系人,一個(gè)視圖用來編輯聯(lián)系人。這個(gè)場(chǎng)景就適合引入狀態(tài)機(jī)。

封裝jQuery的的綁定和觸發(fā)函數(shù):

// jQuery的綁定和觸發(fā)函數(shù)
$(".class").bind("frob.widget", function(event, dataNumber) { 
    console.log(dataNumber)  // => 5
});

$(".class").trigger("frob.widget", 5);

封裝成綁定和觸發(fā)狀態(tài)機(jī):

var Events = {
    bind: function () {
      if (!this.o) {
        this.o = $({});
      }
      this.o.bind.apply(this.o, arguments);
    },

    trigger: function () {
      if (!this.o) {
        this.o = $({});
      }
      this.o.trigger.apply(this.o, arguments);
    }
};

注意bind和trigger都使用apply調(diào)用是因?yàn)橛胊pply傳入了當(dāng)前的引用(this.o)的話,在后續(xù)的事件調(diào)用就解決了上下文問題,不用再使用proxy函數(shù),或者var that = this。

然后創(chuàng)建StateMachine類,主要包含一個(gè)add()函數(shù):

var StateMachine = function() {};
StateMachine.fn = StateMachine.prototype;

// 為StateMachine的實(shí)例添加Events,綁定和觸發(fā)的封裝函數(shù)
$.extend(StateMachine.fn, Events);

// 再為StateMachine的實(shí)例添加add()
StateMachine.fn.add = function (controller) {
  this.bind("change", function(e, current) {
    if (controller == current) {
      controller.activate();
    } else {
      controller.deactivate();
    }
  });

  controller.active = $.proxy(function() {
    this.trigger("change", controller);
  }, this);
};

其實(shí)這個(gè)狀態(tài)機(jī)本質(zhì)上就是發(fā)布/訂閱模型的具體應(yīng)用。

add()函數(shù)就是訂閱,active()函數(shù)就是發(fā)布,當(dāng)調(diào)用active()時(shí),就會(huì)發(fā)布(觸發(fā))控制器的change事件,并且傳入控制器(controller)自己本身作為回調(diào)事件的數(shù)據(jù)參數(shù)(event的后面一個(gè)參數(shù):current)。

狀態(tài)機(jī)目的:如前面說的,控制多個(gè)應(yīng)該互斥顯示的controller之間的激活和非激活狀態(tài),確保controller之間的存在是互斥的,一個(gè)controller顯示了,另一個(gè)就變成非激活狀態(tài),然后消失。

狀態(tài)機(jī)用法

現(xiàn)在有兩個(gè)互斥的controller,各自包含兩個(gè)激活和未激活的函數(shù):

var con1 = {
  activate: function() { /* ... */ },
  deactivate: function() { /* ... */ }
};

var con2 = {
  activate: function() { /* ... */ },
  deactivate: function() { /* ... */ }
};

然后實(shí)例化一個(gè)狀態(tài)機(jī);

var sm = new StateMachine;

然后用add方法添加con1和con2。

sm.add(con1);
sm.add(con2);

現(xiàn)在要激活con1的狀態(tài),則:

con1.activate():
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容