今天进入第二个部分:控制器。
控制器和状态
从以往的开发经验来看,我们都是将状态保存在服务器的session或者本地cookie中,但Javascript应用往往被限制在单页面,所以我们也可以将状态保存在客户端的内存里面。保存在内存还意味着能带来更快的界面响应。
在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);
假设页面内有如下元素
<div id="view">
<button class="assets">按钮</button>
</div>
单击按钮后,将输出”click”,这也暗示了控制器和视图的联系,其实书的这一部分讲的就是如何在控制器内保存状态,根据状态的不同改变不同的视图。
(注)jQuery.proxy(method, context)
$.proxy方法是jQuery中的代理方法,它接收两个参数,返回一个新method,该方法始终保持context上下文。
比如在按钮的监听里:
$("#btn").click(function(){
//这里的this代表按钮
});
$("#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方法:
//控制器1
var con1 = {
activate: function(){
console.log("con1 activate");
},
deactivate: function(){
console.log("con1 deactivate");
}
};
//控制器2
var 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激活的时候显示另一个视图,否则隐藏。这样就能根据状态的不同对视图进行切换。
版权声明:本文为博主原创文章,未经博主允许不得转载。