MVC:控制器controller和状态切换(未完成)

主题:如何使用控制器模式在客户端保持一个状态

包括以下分支:

1.如何将逻辑封装成模块,阻止全局命名空间的污染
2.如何使用视图来进一步简化控制器的结构,以及怎样在视图中实现DOM事件监听
3.路由怎么选择,包括使用URL中的hash片段,使用新的HTML5 History API等技术,以及确保解释两种方法的利弊


1.如何将逻辑封装成模块,阻止全局命名空间的污染

其实就是使用自执行匿名函数。

相关链接 Self-Executing Anonymous Functions

例子:

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

根据链接里的文章,自执行匿名函数可以全局导入,即将全局对象作为参数传入。

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

也可全局导出,有两种方式:

第一种,传入window全局对象

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

第二种,用一个全局对象的this

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

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

小结:通过使用自执行匿名函数可以达到模块化的目的


2.如何使用视图来进一步简化控制器的结构,以及怎样在视图中实现DOM事件监听

为了实现事件回调函数,需要处理上下文问题。上下文问题是指在JS里每次创建函数,这个函数的引用都是window,即this指向的window全局,而嵌套的事件函数需要操作的却又是上级函数的引用,即上级函数的this,这当然就引起了矛盾。

比如:

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

所以需要处理上下文,即处理事件函数的this指向。如果想要自定义作用域的上下文,需要将函数写入一个对象中,比如:

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

这样xxx的作用域,即this就指向的mod对象。

然后怎么用视图来简化控制器的结构能?

这里使用全局this,而不是传入window,来抽象出控制器库,方便控制器的复用。

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

补上用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()来创建并实例化每个具体对应视图元素的控制器。

这里注意用jQuery.ready()的简写jQuery(function($) {...})来确保控制器是在DOM渲染完成后才被加载的。相当于window.onload的功能

jQuery(function($) {
  var ToggleView = Controller.create({
    init: function (view) {
      this.view = $(view);
      // 下面两项是jQuery的事件函数,因此会给回调函数传入event参数,即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的事件函数
    }
  });

  // 实例化控制器,即调用上面定义的,被result继承的init()
  new ToggleView("#view");
});

这里就是一个视图对应一个控制器(具体的代码体现是最后的new ToggleView并传入("#view")参数 ),而一个控制器包含一个或几个相应事件,因此也就是一个视图对应一个或几个事件。

有一个好处是,这个视图#view绑定了这个controller,如果后续要在这个controller里对视图元素查找(可以用$("#view".find("xxx")方法)则限制在#view之下,不会全DOM都查找一遍,从而提高了查找速度。

再一个示例,视图#user的对应控制器:

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

这个被传入的是ID (#users),然后用jQuery的选择器获取(this.el = $(element);)。那么到这都有个问题,视图元素、选择器selector(对应有事件)不多还好,可一旦语义不明显的选择器很多就会显得很乱。

因此这个控制器的不一样在于开辟了一个空间专门存放选择器selector到一个变量的映射表(推荐的写法),这个映射表的实现则是基于示例里私有的两个函数$和refreshElemments。映射表的作用是,在实例化controller之后,就可以用this.xxx(对应的选择器变量名)代替选择器名了,而变量名则可以用语义更清晰的名字,对代码阅读更有帮助,因此可以让代码更简洁易读(之后还有对事件的映射,创建相应映射表和映射函数,效果相同)。

tips:注意是选择器名称在前,对应的变量名在后,这样在映射时才能正确对应(事件映射相同)

(待添加事件映射示例)


状态机

mvc结合状态机在某一对象有多种状态且经常需要转换的时候,使用状态机实现非常方便。在model层给对象添加状态机组件,然后在触发某种状态时(onstart,onready,onrun…)分发事件,然后再view层监听此事件,当model处于某种状态时,触发相应的事件,view层监听到事件后做出不同的动作。关于mvc、状态机的使用可以查看sample下的demo

完整示例:状态机完整示例代码

状态机本质上由两部分组成:状态和转换器。

它只有一个活动状态,也包含很多非活动状态。当活动状态之间相互切换时就会调用状态转换器。

状态机的工作场景:

存在两个视图,他们的存在是互斥关系,其中一个显示时,另一个就是隐藏的。比如联系人列表,一个视图用来显示联系人,一个视图用来编辑联系人。这个场景就适合引入状态机。

封装jQuery的的绑定和触发函数:

// jQuery的绑定和触发函数
$(".class").bind("frob.widget", function(event, dataNumber) { 
    console.log(dataNumber)  // => 5
});

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

封装成绑定和触发状态机:

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调用是因为用apply传入了当前的引用(this.o)的话,在后续的事件调用就解决了上下文问题,不用再使用proxy函数,或者var that = this。

然后创建StateMachine类,主要包含一个add()函数:

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

// 为StateMachine的实例添加Events,绑定和触发的封装函数
$.extend(StateMachine.fn, Events);

// 再为StateMachine的实例添加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);
};

其实这个状态机本质上就是发布/订阅模型的具体应用。

add()函数就是订阅,active()函数就是发布,当调用active()时,就会发布(触发)控制器的change事件,并且传入控制器(controller)自己本身作为回调事件的数据参数(event的后面一个参数:current)。

状态机目的:如前面说的,控制多个应该互斥显示的controller之间的激活和非激活状态,确保controller之间的存在是互斥的,一个controller显示了,另一个就变成非激活状态,然后消失。

状态机用法

现在有两个互斥的controller,各自包含两个激活和未激活的函数:

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

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

然后实例化一个状态机;

var sm = new StateMachine;

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

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

现在要激活con1的状态,则:

con1.activate():

你可能感兴趣的:(MVC:控制器controller和状态切换(未完成))