今天进入第二个部分:控制器。
控制器和状态
从以往的开发经验来看。我们都是将状态保存在server的session或者本地cookie中,但Javascript应用往往被限制在单页面,所以我们也能够将状态保存在client的内存里面。保存在内存还意味着能带来更快的界面响应。
在MVC里面,状态都是保存在控制器里的,控制器相当于应用中视图和模型的纽带。
当载入页面的时候,控制器将事件处理程序绑定在视图里。并适时处理回调,以及和模型必要的对接。
控制器是模块化的并且非常独立,理想状况下不应该定义不论什么全局变量,而是应该定义为全然解耦的功能组件。
所以我们通过模块模式将控制器放在一个马上处理的匿名函数里并传递全局參数进去,避免在内部訪问全局变量时须要遍历作用域,也清晰地表明了这个模块使用了哪些全局变量。
(function($){ var mod = {}; //改变參数的上下文并马上运行 mod.load = function(func){ $($.proxy(func, this)); }; //点击事件 mod.assetsClick = function(){ //处理点击 console.log("click!"); } //调用load方法 mod.load(function(){ //为元素加入单击事件 this.view = $("#view"); this.view.find(".assets").click( $.proxy(this.assetsClick, this) ); });})(jQuery);
假设页面内有例如以下元素
单击button后。将输出”click”,这也暗示了控制器和视图的联系。事实上书的这一部分讲的就是怎样在控制器内保存状态,依据状态的不同改变不同的视图。
(注)jQuery.proxy(method, context)
$.proxy方法是jQuery中的代理方法,它接收两个參数。返回一个新method,该方法始终保持context上下文。
比方在button的监听里:$("#btn").click(function(){ //这里的this代表button});
$("#btn").click( $.proxy(function(){ //这里的this代表window }, window););
(注解完)
抽象出库
如今将控制器抽象成库,并加入一些新的方法。这样就能在外部或别的模块中重用它了。
(function($, exports){ //假设通过构造方法传递參数,则调用include方法 var mod = function(includes){ if(includes){ this.include(includes); } }; //改变原型对象名(便于调用) mod.fn = mod.prototype; //定义自己的代理方法。上下文始终指向控制器自己 mod.fn.proxy = function(func){ return $.proxy(func, this); }; //马上运行函数 mod.fn.load = function(func){ $(this.proxy(func)); }; //为构造器加入方法 mod.fn.include = function(ob){ $.extend(this, ob); }; //将构造器暴露出全局,在外界也能够訪问 exports.Controller = mod;})(jQuery, window);
在别的地方须要用到控制器的时候,就调用Controller就可以:
(function($, Controller){ //创建控制器 var mod = new Controller(); //改变view的类名 mod.toggleClass = function(e){ //jQuery中的toggleClass方法代表假设存在此类名则删除,不存在则加入 this.view.toggleClass("over"); } //页面载入完马上运行 mod.load(function(){ //绑定页面元素,加入监听 this.view = $("#view"); this.view.mouseover(this.proxy(this.toggleClass)); this.view.mouseout(this.proxy(this.toggleClass)); });})(jQuery, Controller)
在以上的使用中,通过匿名函数马上调用的方法并非在DOM载入之后载入的。而是在生成DOM之前,然而控制器的load方法又是在页面文档载入完毕之后才进行回调。我们能够对控制器进行进一步改写,在DOM生成之后统一载入控制器。
//定义一个全局对象var exports = this;(function($){ var mod = {}; //提供一个create方法,生成控制器 mod.create = function(includes){ //create方法返回的就是result,一种控制器 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);
在创建控制器的时候必须手动指定init方法,在init方法为dom元素加入监听:
$(function(){ //创建控制器 var ToggleView = Controller.create({ init: function(view){ this.view = $(view); this.view.mouseover(this.proxy(this.toggleClass)); this.view.mouseout(this.proxy(this.toggleClass)); }, toggleClass: function(){ this.view.toggleClass("over"); } }); //创建一个控制器的实例 new ToggleView("#view");});
在创建实例的时候将在构造函数里触发init事件。另外依据实例化的情况将视图传入控制器而不是写死在控制器内,我们就能够将控制器重用于不同的元素。同一时候保持代码最短。
訪问视图
一种常见的模式是一个视图相应一个控制器,视图包括Id,而在控制器内使用视图的元素则使用class,这样和其它视图的元素不会产生冲突,比方上面的ToggleView
传入了Id为view的元素,所以在view内的元素则使用类名进行訪问。
init: function(view){ this.view = $(view); this.form = this.view.find(".form");}
但这意味着控制器中会有非常多选择器,须要不断查找DOM,我们能够在控制器中专门开辟一个空间来存放选择器到变量的映射表:
elements: { "form.searchForm": "searchForm", "form input[type=text]": "searchInput"}
有了这种映射表之后。控制器的属性名(比方searchForm)就能和详细的元素(类名为searchForm的form)相相应了,并且在控制器实例化的时候创建他们:
$(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){ console.log("Searching", this.searchInput.val()); }, //内部使用的选择器,将上下文指定为视图元素 $: function(selector){ return $(selector, this.el); }, //创建视图内的元素 refreshElements: function(){ for(var key in this.elements){ //key为选择器名。值为属性名 this[this.elements[key]] = this.$(key); } } }); //视图用id指定 new SearchView("#users");});
状态机
状态机是“有限状态机”的简称,本质上由两部分构成:状态和转换器。它仅仅有一个活动状态,但也包括非常多非活动状态。当活动状态之间相互切换的时候就会调用状态转换器。
比方应用中存在非常多视图,它们的显示是相互独立的,一个视图用来显示联系人,一个视图用来编辑联系人,这两个视图一定是相互排斥关系,当中一个显示还有一个一定隐藏,这个场景就非常适合引入状态机。由于它能确保每一个时刻仅仅有一个是激活的。
首先看一下状态机的思路。我们构造一个状态机:
var StateMachine = function(){ };StateMachine.fn = StateMechine.prototype;StateMachine.fn.bind = function(){ if(!this.o){ this.o = $({}); } //绑定自己定义事件 this.o.bind.apply(this.o, arguments);}StateMachine.fn.trigger = function(){ if(!this.o){ this.o = $({}); } //触发自己定义事件 this.o.trigger.apply(this.o, arguments);}StateMechine.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);}
(注)jQuery中的自己定义事件
上述代码重点在于bind和trigger,在jQuery中,利用这两个方法能够非常轻易地实现自己定义事件:
var $obj = $({});$obj.bind("myEvent", function(){ console.log("自己定义事件");});$obj.trigger("myEvent"); //"自己定义事件"
bind代表绑定。trigger表示触发,你仅仅须要一个jQuery对象就够了。所以上面创建了一个空的对象,使用$包装成了jQuery对象。
(注解完)
这个状态机的add()方法将传入的控制器加入至状态列表,并为状态机中的o绑定一个自己定义的change事件(加入一个绑定一个)。然后创建一个active()函数。当控制器调用active()的时候。触发所有的change事件,则除了调用的控制器将运行activate方法外。其它控制器所有运行deactivate方法:
//控制器1var con1 = { activate: function(){ console.log("con1 activate"); }, deactivate: function(){ console.log("con1 deactivate"); }};//控制器2var con2 = { activate: function(){ console.log("con2 activate"); }, deactivate: function(){ console.log("con2 deactivate"); }};var sm = new StateMachine();sm.add(con1);sm.add(con2);con1.active(); //输出"con1 activate"和"con2 deactivate"
当然也能够直接通过状态机触发
sm.trigger("change", con1);
通过状态机的切换状态,我们能够结合控制器改变视图,当con1激活的时候显示一个视图,否则隐藏;con2激活的时候显示还有一个视图,否则隐藏。
这样就能依据状态的不同对视图进行切换。