主题:如何使用控制器模式在客户端保持一个状态
包括以下分支:
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():