主題:如何使用控制器模式在客戶端保持一個(gè)狀態(tài)
包括以下分支:
1.如何將邏輯封裝成模塊蜒车,阻止全局命名空間的污染
2.如何使用視圖來(lái)進(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.如何使用視圖來(lái)進(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ì)象碴萧。
然后怎么用視圖來(lái)簡(jiǎn)化控制器的結(jié)構(gòu)能?
這里使用全局this末购,而不是傳入window破喻,來(lái)抽象出控制器庫(kù),方便控制器的復(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);
下面則是用控制器庫(kù)的API:Controller.create()來(lái)創(chuàng)建并實(shí)例化每個(gè)具體對(duì)應(yīng)視圖元素的控制器曹质。
這里注意用jQuery.ready()的簡(jiǎn)寫jQuery(function($) {...})來(lái)確保控制器是在DOM渲染完成后才被加載的擎场。相當(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的選擇器獲取(this.el = $(element);)绰咽。那么到這都有個(gè)問題菇肃,視圖元素、選擇器selector(對(duì)應(yīng)有事件)不多還好剃诅,可一旦語(yǔ)義不明顯的選擇器很多就會(huì)顯得很亂巷送。
因此這個(gè)控制器的不一樣在于開辟了一個(gè)空間專門存放選擇器selector到一個(gè)變量的映射表(推薦的寫法)驶忌,這個(gè)映射表的實(shí)現(xiàn)則是基于示例里私有的兩個(gè)函數(shù)$和refreshElemments矛辕。映射表的作用是,在實(shí)例化controller之后付魔,就可以用this.xxx(對(duì)應(yīng)的選擇器變量名)代替選擇器名了聊品,而變量名則可以用語(yǔ)義更清晰的名字,對(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è)視圖用來(lái)顯示聯(lián)系人桶唐,一個(gè)視圖用來(lái)編輯聯(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():