瀏覽器前端編程的面貌自2005年以來已經(jīng)發(fā)生了深刻的變化,這并不簡(jiǎn)單的意味著出現(xiàn)了大量功能豐富的基礎(chǔ)庫(kù),使得我們可以更加方便的編寫業(yè)務(wù)代碼,更重要的是我們看待前端技術(shù)的觀念發(fā)生了重大轉(zhuǎn)變,明確意識(shí)到了如何以前端特有的方式釋放程序員的生產(chǎn)力。本文將結(jié)合jQuery源碼的實(shí)現(xiàn)原理,對(duì)javascript中涌現(xiàn)出的編程范式和常用技巧作一簡(jiǎn)單介紹。
1. AJAX: 狀態(tài)駐留,異步更新
首先來看一點(diǎn)歷史。
A. 1995年Netscape公司的Brendan Eich開發(fā)了javacript語言,這是一種動(dòng)態(tài)(dynamic)、弱類型(weakly typed)、基于原型(prototype-based)的腳本語言。
B. 1999年微軟IE5發(fā)布,其中包含了XMLHTTP ActiveX控件。
C. 2001年微軟IE6發(fā)布,部分支持DOM level 1和CSS 2標(biāo)準(zhǔn)。
D. 2002年Douglas Crockford發(fā)明JSON格式。
至此,可以說Web2.0所依賴的技術(shù)元素已經(jīng)基本成形,但是并沒有立刻在整個(gè)業(yè)界產(chǎn)生重大的影響。盡管一些“頁(yè)面異步局部刷新”的技巧在程序員中間秘密的流傳,甚至催生了bindows這樣龐大臃腫的類庫(kù),但總的來說,前端被看作是貧瘠而又骯臟的沼澤地,只有后臺(tái)技術(shù)才是王道。到底還缺少些什么呢?
當(dāng)我們站在今天的角度去回顧2005年之前的js代碼,包括那些當(dāng)時(shí)的牛人所寫的代碼,可以明顯的感受到它們?cè)诔绦蚩刂屏ι系腻钊?。并不是說2005年之前的js技術(shù)本身存在問題,只是它們?cè)诟拍顚用嫔鲜且槐P散沙,缺乏統(tǒng)一的觀念,或者說缺少自己獨(dú)特的風(fēng)格, 自己的靈魂。當(dāng)時(shí)大多數(shù)的人,大多數(shù)的技術(shù)都試圖在模擬傳統(tǒng)的面向?qū)ο笳Z言,利用傳統(tǒng)的面向?qū)ο蠹夹g(shù),去實(shí)現(xiàn)傳統(tǒng)的GUI模型的仿制品。
2005年是變革的一年,也是創(chuàng)造概念的一年。伴隨著Google一系列讓人耳目一新的交互式應(yīng)用的發(fā)布,Jesse James Garrett的一篇文章《Ajax: A New Approach to Web Applications》被廣為傳播。Ajax這一前端特有的概念迅速將眾多分散的實(shí)踐統(tǒng)一在同一口號(hào)之下,引發(fā)了Web編程范式的轉(zhuǎn)換。所謂名不正則言不順,這下無名群眾可找到組織了。在未有Ajax之前,人們?cè)缫颜J(rèn)識(shí)到了B/S架構(gòu)的本質(zhì)特征在于瀏覽器和服務(wù)器的狀態(tài)空間是分離的,但是一般的解決方案都是隱藏這一區(qū)分,將前臺(tái)狀態(tài)同步到后臺(tái),由后臺(tái)統(tǒng)一進(jìn)行邏輯處理,例如ASP.NET。因?yàn)槿狈Τ墒斓脑O(shè)計(jì)模式支持前臺(tái)狀態(tài)駐留,在換頁(yè)的時(shí)候,已經(jīng)裝載的js對(duì)象將被迫被丟棄,這樣誰還能指望它去完成什么復(fù)雜的工作嗎?
Ajax明確提出界面是局部刷新的,前臺(tái)駐留了狀態(tài),這就促成了一種需要:需要js對(duì)象在前臺(tái)存在更長(zhǎng)的時(shí)間。這也就意味著需要將這些對(duì)象和功能有效的管理起來,意味著更復(fù)雜的代碼組織技術(shù),意味著對(duì)模塊化,對(duì)公共代碼基的渴求。
jQuery現(xiàn)有的代碼中真正與Ajax相關(guān)(使用XMLHTTP控件異步訪問后臺(tái)返回?cái)?shù)據(jù))的部分其實(shí)很少,但是如果沒有Ajax, jQuery作為公共代碼基也就缺乏存在的理由。
2. 模塊化:管理名字空間
當(dāng)大量的代碼產(chǎn)生出來以后,我們所需要的最基礎(chǔ)的概念就是模塊化,也就是對(duì)工作進(jìn)行分解和復(fù)用。工作得以分解的關(guān)鍵在于各人獨(dú)立工作的成果可以集成在一起。這意味著各個(gè)模塊必須基于一致的底層概念,可以實(shí)現(xiàn)交互,也就是說應(yīng)該基于一套公共代碼基,屏蔽底層瀏覽器的不一致性,并實(shí)現(xiàn)統(tǒng)一的抽象層,例如統(tǒng)一的事件管理機(jī)制等。比統(tǒng)一代碼基更重要的是,各個(gè)模塊之間必須沒有名字沖突。否則,即使兩個(gè)模塊之間沒有任何交互,也無法共同工作。
jQuery目前鼓吹的主要賣點(diǎn)之一就是對(duì)名字空間的良好控制。這甚至比提供更多更完善的功能點(diǎn)都重要的多。良好的模塊化允許我們復(fù)用任何來源的代碼,所有人的工作得以積累疊加。而功能實(shí)現(xiàn)僅僅是一時(shí)的工作量的問題。jQuery使用module pattern的一個(gè)變種來減少對(duì)全局名字空間的影響,僅僅在window對(duì)象上增加了一個(gè)jQuery對(duì)象(也就是$函數(shù))。
所謂的module pattern代碼如下,它的關(guān)鍵是利用匿名函數(shù)限制臨時(shí)變量的作用域。
var feature =(function() {
// 私有變量和函數(shù)
var privateThing = 'secret',
publicThing = 'not secret',
changePrivateThing = function() {
privateThing = 'super secret';
},
sayPrivateThing = function() {
console.log(privateThing);
changePrivateThing();
};
// 返回對(duì)外公開的API
return {
publicThing : publicThing,
sayPrivateThing : sayPrivateThing
}
})();
js本身缺乏包結(jié)構(gòu),不過經(jīng)過多年的嘗試之后業(yè)內(nèi)已經(jīng)逐漸統(tǒng)一了對(duì)包加載的認(rèn)識(shí),形成了RequireJs庫(kù)這樣得到一定共識(shí)的解決方案。jQuery可以與RequireJS庫(kù)良好的集成在一起, 實(shí)現(xiàn)更完善的模塊依賴管理。http://requirejs.org/docs/jquery.html
require(["jquery", "jquery.my"], function() {
//當(dāng)jquery.js和jquery.my.js都成功裝載之后執(zhí)行
$(function(){
$('#my').myFunc();
});
});
通過以下函數(shù)調(diào)用來定義模塊my/shirt, 它依賴于my/cart和my/inventory模塊,
require.def("my/shirt",
["my/cart", "my/inventory"],
function(cart, inventory) {
// 這里使用module pattern來返回my/shirt模塊對(duì)外暴露的API
return {
color: "blue",
size: "large"
addToCart: function() {
// decrement是my/inventory對(duì)外暴露的API
inventory.decrement(this);
cart.add(this);
}
}
}
);
3. 神奇的$:對(duì)象提升
當(dāng)你第一眼看到$函數(shù)的時(shí)候,你想到了什么?傳統(tǒng)的編程理論總是告訴我們函數(shù)命名應(yīng)該準(zhǔn)確,應(yīng)該清晰無誤的表達(dá)作者的意圖,甚至聲稱長(zhǎng)名字要優(yōu)于短名字,因?yàn)闇p少了出現(xiàn)歧義的可能性。但是,$是什么?亂碼?它所傳遞的信息實(shí)在是太隱晦,太曖昧了。$是由prototype.js庫(kù)發(fā)明的,它真的是一個(gè)神奇的函數(shù),因?yàn)樗梢詫⒁粋€(gè)原始的DOM節(jié)點(diǎn)提升(enhance)為一個(gè)具有復(fù)雜行為的對(duì)象。在prototype.js最初的實(shí)現(xiàn)中,$函數(shù)的定義為
var $ = function (id) {
return "string" == typeof id ? document.getElementById(id) : id;
};
這基本對(duì)應(yīng)于如下公式
e = $(id)
這絕不僅僅是提供了一個(gè)聰明的函數(shù)名稱縮寫,更重要的是在概念層面上建立了文本id與DOM element之間的一一對(duì)應(yīng)。在未有$之前,id與對(duì)應(yīng)的element之間的距離十分遙遠(yuǎn),一般要將element緩存到變量中,例如
var ea = docuement.getElementById('a');
var eb = docuement.getElementById('b');
ea.style....
但是使用$之后,卻隨處可見如下的寫法
$('header_'+id).style...
$('body_'+id)....
id與element之間的距離似乎被消除了,可以非常緊密的交織在一起。
prototype.js后來擴(kuò)展了$的含義,
function $() {
var elements = new Array();
for (var i = 0; i < arguments.length; i++) {
var element = arguments[i];
if (typeof element == 'string')
element = document.getElementById(element);
if (arguments.length == 1)
return element;
elements.push(element);
}
return elements;
}
這對(duì)應(yīng)于公式
[e,e] = $(id,id)
很遺憾,這一步prototype.js走偏了,這一做法很少有實(shí)用的價(jià)值。
真正將$發(fā)揚(yáng)光大的是jQuery, 它的$對(duì)應(yīng)于公式
[o] = $(selector)
這里有三個(gè)增強(qiáng)
A. selector不再是單一的節(jié)點(diǎn)定位符,而是復(fù)雜的集合選擇符
B. 返回的元素不是原始的DOM節(jié)點(diǎn),而是經(jīng)過jQuery進(jìn)一步增強(qiáng)的具有豐富行為的對(duì)象,可以啟動(dòng)復(fù)雜的函數(shù)調(diào)用鏈。
C. $返回的包裝對(duì)象被造型為數(shù)組形式,將集合操作自然的整合到調(diào)用鏈中。
當(dāng)然,以上僅僅是對(duì)神奇的$的一個(gè)過分簡(jiǎn)化的描述,它的實(shí)際功能要復(fù)雜得多. 特別是有一個(gè)非常常用的直接構(gòu)造功能.
$("<table><tbody><tr><td>...</td></tr></tbody></table>")....
jQuery將根據(jù)傳入的html文本直接構(gòu)造出一系列的DOM節(jié)點(diǎn),并將其包裝為jQuery對(duì)象. 這在某種程度上可以看作是對(duì)selector的擴(kuò)展: html內(nèi)容描述本身就是一種唯一指定.
$(function{})這一功能就實(shí)在是讓人有些無語了, 它表示當(dāng)document.ready的時(shí)候調(diào)用此回調(diào)函數(shù)。真的,$是一個(gè)神奇的函數(shù), 有任何問題,請(qǐng)$一下。
總結(jié)起來, $是從普通的DOM和文本描述世界到具有豐富對(duì)象行為的jQuery世界的躍遷通道??邕^了這道門,就來到了理想國(guó)。
4. 無定形的參數(shù):專注表達(dá)而不是約束
弱類型語言既然頭上頂著個(gè)"弱"字, 總難免讓人有些先天不足的感覺. 在程序中缺乏類型約束, 是否真的是一種重大的缺憾? 在傳統(tǒng)的強(qiáng)類型語言中, 函數(shù)參數(shù)的類型,個(gè)數(shù)等都是由編譯器負(fù)責(zé)檢查的約束條件, 但這些約束仍然是遠(yuǎn)遠(yuǎn)不夠的. 一般應(yīng)用程序中為了加強(qiáng)約束, 總會(huì)增加大量防御性代碼, 例如在C++中我們常用ASSERT, 而在java中也經(jīng)常需要判斷參數(shù)值的范圍
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: "+size);
很顯然, 這些代碼將導(dǎo)致程序中存在大量無功能的執(zhí)行路徑, 即我們做了大量判斷, 代碼執(zhí)行到某個(gè)點(diǎn), 系統(tǒng)拋出異常, 大喊此路不通. 如果我們換一個(gè)思路, 既然已經(jīng)做了某種判斷,能否利用這些判斷的結(jié)果來做些什么呢? javascript是一種弱類型的語言,它是無法自動(dòng)約束參數(shù)類型的, 那如果順勢(shì)而行,進(jìn)一步弱化參數(shù)的形態(tài), 將"弱"推進(jìn)到一種極致, 在弱無可弱的時(shí)候, weak會(huì)不會(huì)成為標(biāo)志性的特點(diǎn)?
看一下jQuery中的事件綁定函數(shù)bind,
A. 一次綁定一個(gè)事件 $("#my").bind("mouseover", function(){});
B. 一次綁定多個(gè)事件 $("#my").bind("mouseover mouseout",function(){})
C. 換一個(gè)形式, 同樣綁定多個(gè)事件
$("#my").bind({mouseover:function(){}, mouseout:function(){});
D. 想給事件監(jiān)聽器傳點(diǎn)參數(shù)
$('#my').bind('click', {foo: "xxxx"}, function(event) { event.data.foo..})
E. 想給事件監(jiān)聽器分個(gè)組
$("#my").bind("click.myGroup″, function(){});
F. 這個(gè)函數(shù)為什么還沒有瘋掉???
就算是類型不確定, 在固定位置上的參數(shù)的意義總要是確定的吧? 退一萬步來說, 就算是參數(shù)位置不重要了,函數(shù)本身的意義應(yīng)該是確定的吧? 但這是什么?
取值 value = o.val(), 設(shè)置值 o.val(3)
一個(gè)函數(shù)怎么可以這樣過分, 怎么能根據(jù)傳入?yún)?shù)的類型和個(gè)數(shù)不同而行為不同呢? 看不順眼是不是? 可這就是俺們的價(jià)值觀. 既然不能防止, 那就故意允許. 雖然形式多變, 卻無一句廢話. 缺少約束, 不妨礙表達(dá)(我不是出來嚇人的).
5. 鏈?zhǔn)讲僮? 線性化的逐步細(xì)化
jQuery早期最主要的賣點(diǎn)就是所謂的鏈?zhǔn)讲僮?chain).
$('#content') // 找到content元素
.find('h3') // 選擇所有后代h3節(jié)點(diǎn)
.eq(2) // 過濾集合, 保留第三個(gè)元素
.html('改變第三個(gè)h3的文本')
.end() // 返回上一級(jí)的h3集合
.eq(0)
.html('改變第一個(gè)h3的文本');
在一般的命令式語言中, 我們總需要在重重嵌套循環(huán)中過濾數(shù)據(jù), 實(shí)際操作數(shù)據(jù)的代碼與定位數(shù)據(jù)的代碼糾纏在一起. 而jQuery采用先構(gòu)造集合然后再應(yīng)用函數(shù)于集合的方式實(shí)現(xiàn)兩種邏輯的解耦, 實(shí)現(xiàn)嵌套結(jié)構(gòu)的線性化. 實(shí)際上, 我們并不需要借助過程化的思想就可以很直觀的理解一個(gè)集合, 例如 $('div.my input:checked')可以看作是一種直接的描述,而不是對(duì)過程行為的跟蹤.
循環(huán)意味著我們的思維處于一種反復(fù)回繞的狀態(tài), 而線性化之后則沿著一個(gè)方向直線前進(jìn), 極大減輕了思維負(fù)擔(dān), 提高了代碼的可組合性. 為了減少調(diào)用鏈的中斷, jQuery發(fā)明了一個(gè)絕妙的主意: jQuery包裝對(duì)象本身類似數(shù)組(集合). 集合可以映射到新的集合, 集合可以限制到自己的子集合,調(diào)用的發(fā)起者是集合,返回結(jié)果也是集合,集合可以發(fā)生結(jié)構(gòu)上的某種變化但它還是集合, 集合是某種概念上的不動(dòng)點(diǎn),這是從函數(shù)式語言中吸取的設(shè)計(jì)思想。集合操作是太常見的操作, 在java中我們很容易發(fā)現(xiàn)大量所謂的封裝函數(shù)其實(shí)就是在封裝一些集合遍歷操作, 而在jQuery中集合操作因?yàn)樘卑锥恍枰庋b.
鏈?zhǔn)秸{(diào)用意味著我們始終擁有一個(gè)“當(dāng)前”對(duì)象,所有的操作都是針對(duì)這一當(dāng)前對(duì)象進(jìn)行。這對(duì)應(yīng)于如下公式
x += dx
調(diào)用鏈的每一步都是對(duì)當(dāng)前對(duì)象的增量描述,是針對(duì)最終目標(biāo)的逐步細(xì)化過程。Witrix平臺(tái)中對(duì)這一思想也有著廣泛的應(yīng)用。特別是為了實(shí)現(xiàn)平臺(tái)機(jī)制與業(yè)務(wù)代碼的融合,平臺(tái)會(huì)提供對(duì)象(容器)的缺省內(nèi)容,而業(yè)務(wù)代碼可以在此基礎(chǔ)上進(jìn)行逐步細(xì)化的修正,包括取消缺省的設(shè)置等。
話說回來, 雖然表面上jQuery的鏈?zhǔn)秸{(diào)用很簡(jiǎn)單, 內(nèi)部實(shí)現(xiàn)的時(shí)候卻必須自己多寫一層循環(huán), 因?yàn)榫幾g器并不知道"自動(dòng)應(yīng)用于集合中每個(gè)元素"這回事.
$.fn['someFunc'] = function(){
return this.each(function(){
jQuery.someFunc(this,...);
}
}
6. data: 統(tǒng)一數(shù)據(jù)管理
作為一個(gè)js庫(kù),它必須解決的一個(gè)大問題就是js對(duì)象與DOM節(jié)點(diǎn)之間的狀態(tài)關(guān)聯(lián)與協(xié)同管理問題。有些js庫(kù)選擇以js對(duì)象為主,在js對(duì)象的成員變量中保存DOM節(jié)點(diǎn)指針,訪問時(shí)總是以js對(duì)象為入口點(diǎn),通過js函數(shù)間接操作DOM對(duì)象。在這種封裝下,DOM節(jié)點(diǎn)其實(shí)只是作為界面展現(xiàn)的一種底層“匯編”而已。jQuery的選擇與Witrix平臺(tái)類似,都是以HTML自身結(jié)構(gòu)為基礎(chǔ),通過js增強(qiáng)(enhance)DOM節(jié)點(diǎn)的功能,將它提升為一個(gè)具有復(fù)雜行為的擴(kuò)展對(duì)象。這里的思想是非侵入式設(shè)計(jì)(non-intrusive)和優(yōu)雅退化機(jī)制(graceful degradation)。語義結(jié)構(gòu)在基礎(chǔ)的HTML層面是完整的,js的作用是增強(qiáng)了交互行為,控制了展現(xiàn)形式。
如果每次我們都通過$('#my')的方式來訪問相應(yīng)的包裝對(duì)象,那么一些需要長(zhǎng)期保持的狀態(tài)變量保存在什么地方呢?jQuery提供了一個(gè)統(tǒng)一的全局?jǐn)?shù)據(jù)管理機(jī)制。
獲取數(shù)據(jù) $('#my').data('myAttr') 設(shè)置數(shù)據(jù) $('#my').data('myAttr',3);
這一機(jī)制自然融合了對(duì)HTML5的data屬性的處理
<input id="my" data-my-attr="4" ... />
通過 $('#my').data('myAttr')將可以讀取到HTML中設(shè)置的數(shù)據(jù)。
第一次訪問data時(shí),jQuery將為DOM節(jié)點(diǎn)分配一個(gè)唯一的uuid, 然后設(shè)置在DOM節(jié)點(diǎn)的一個(gè)特定的expando屬性上, jQuery保證這個(gè)uuid在本頁(yè)面中不重復(fù)。
elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
以上代碼可以同時(shí)處理DOM節(jié)點(diǎn)和純js對(duì)象的情況。如果是js對(duì)象,則data直接放置在js對(duì)象自身中,而如果是DOM節(jié)點(diǎn),則通過cache統(tǒng)一管理。
因?yàn)樗械臄?shù)據(jù)都是通過data機(jī)制統(tǒng)一管理的,特別是包括所有事件監(jiān)聽函數(shù)(data.events),因此jQuery可以安全的實(shí)現(xiàn)資源管理。在clone節(jié)點(diǎn)的時(shí)候,可以自動(dòng)clone其相關(guān)的事件監(jiān)聽函數(shù)。而當(dāng)DOM節(jié)點(diǎn)的內(nèi)容被替換或者DOM節(jié)點(diǎn)被銷毀的時(shí)候,jQuery也可以自動(dòng)解除事件監(jiān)聽函數(shù), 并安全的釋放相關(guān)的js數(shù)據(jù)。
7. event:統(tǒng)一事件模型
"事件沿著對(duì)象樹傳播"這一圖景是面向?qū)ο蠼缑婢幊棠P偷木杷?。?duì)象的復(fù)合構(gòu)成對(duì)界面結(jié)構(gòu)的一個(gè)穩(wěn)定的描述,事件不斷在對(duì)象樹的某個(gè)節(jié)點(diǎn)發(fā)生,并通過冒泡機(jī)制向上傳播。對(duì)象樹很自然的成為一個(gè)控制結(jié)構(gòu),我們可以在父節(jié)點(diǎn)上監(jiān)聽所有子節(jié)點(diǎn)上的事件,而不用明確與每一個(gè)子節(jié)點(diǎn)建立關(guān)聯(lián)。
jQuery除了為不同瀏覽器的事件模型建立了統(tǒng)一抽象之外,主要做了如下增強(qiáng):
A. 增加了自定制事件(custom)機(jī)制. 事件的傳播機(jī)制與事件內(nèi)容本身原則上是無關(guān)的, 因此自定制事件完全可以和瀏覽器內(nèi)置事件通過同一條處理路徑, 采用同樣的監(jiān)聽方式. 使用自定制事件可以增強(qiáng)代碼的內(nèi)聚性, 減少代碼耦合. 例如如果沒有自定制事件, 關(guān)聯(lián)代碼往往需要直接操作相關(guān)的對(duì)象
$('.switch, .clapper').click(function() {
var $light = $(this).parent().find('.lightbulb');
if ($light.hasClass('on')) {
$light.removeClass('on').addClass('off');
} else {
$light.removeClass('off').addClass('on');
}
});
而如果使用自定制事件,則表達(dá)的語義更加內(nèi)斂明確,
$('.switch, .clapper').click(function() {
$(this).parent().find('.lightbulb').trigger('changeState');
});
B. 增加了對(duì)動(dòng)態(tài)創(chuàng)建節(jié)點(diǎn)的事件監(jiān)聽. bind函數(shù)只能將監(jiān)聽函數(shù)注冊(cè)到已經(jīng)存在的DOM節(jié)點(diǎn)上. 例如
$('li.trigger').bind('click',function(){}}
如果調(diào)用bind之后,新建了另一個(gè)li節(jié)點(diǎn),則該節(jié)點(diǎn)的click事件不會(huì)被監(jiān)聽.
jQuery的delegate機(jī)制可以將監(jiān)聽函數(shù)注冊(cè)到父節(jié)點(diǎn)上, 子節(jié)點(diǎn)上觸發(fā)的事件會(huì)根據(jù)selector被自動(dòng)派發(fā)到相應(yīng)的handlerFn上. 這樣一來現(xiàn)在注冊(cè)就可以監(jiān)聽未來創(chuàng)建的節(jié)點(diǎn).
$('#myList').delegate('li.trigger', 'click', handlerFn);
最近jQuery1.7中統(tǒng)一了bind, live和delegate機(jī)制, 天下一統(tǒng), 只有on/off.
$('li.trigger’).on('click', handlerFn); // 相當(dāng)于bind
$('#myList’).on('click', 'li.trigger', handlerFn); // 相當(dāng)于delegate
8. 動(dòng)畫隊(duì)列:全局時(shí)鐘協(xié)調(diào)
拋開jQuery的實(shí)現(xiàn)不談, 先考慮一下如果我們要實(shí)現(xiàn)界面上的動(dòng)畫效果, 到底需要做些什么? 比如我們希望將一個(gè)div的寬度在1秒鐘之內(nèi)從100px增加到200px. 很容易想見, 在一段時(shí)間內(nèi)我們需要不時(shí)的去調(diào)整一下div的寬度, [同時(shí)]我們還需要執(zhí)行其他代碼. 與一般的函數(shù)調(diào)用不同的是, 發(fā)出動(dòng)畫指令之后, 我們不能期待立刻得到想要的結(jié)果, 而且我們不能原地等待結(jié)果的到來. 動(dòng)畫的復(fù)雜性就在于:一次性表達(dá)之后要在一段時(shí)間內(nèi)執(zhí)行,而且有多條邏輯上的執(zhí)行路徑要同時(shí)展開, 如何協(xié)調(diào)?
偉大的艾薩克.牛頓爵士在《自然哲學(xué)的數(shù)學(xué)原理》中寫道:"絕對(duì)的、真正的和數(shù)學(xué)的時(shí)間自身在流逝著". 所有的事件可以在時(shí)間軸上對(duì)齊, 這就是它們內(nèi)在的協(xié)調(diào)性. 因此為了從步驟A1執(zhí)行到A5, 同時(shí)將步驟B1執(zhí)行到B5, 我們只需要在t1時(shí)刻執(zhí)行[A1, B1], 在t2時(shí)刻執(zhí)行[A2,B2], 依此類推.
t1 | t2 | t3 | t4 | t5 ...
A1 | A2 | A3 | A4 | A5 ...
B1 | B2 | B3 | B4 | B5 ...
具體的一種實(shí)現(xiàn)形式可以是
A. 對(duì)每個(gè)動(dòng)畫, 將其分裝為一個(gè)Animation對(duì)象, 內(nèi)部分成多個(gè)步驟.
animation = new Animation(div,"width",100,200,1000,
負(fù)責(zé)步驟切分的插值函數(shù),動(dòng)畫執(zhí)行完畢時(shí)的回調(diào)函數(shù));
B. 在全局管理器中注冊(cè)動(dòng)畫對(duì)象
timerFuncs.add(animation);
C. 在全局時(shí)鐘的每一個(gè)觸發(fā)時(shí)刻, 將每個(gè)注冊(cè)的執(zhí)行序列推進(jìn)一步, 如果已經(jīng)結(jié)束, 則從全局管理器中刪除.
for each animation in timerFuncs
if(!animation.doOneStep())
timerFuncs.remove(animation)
解決了原理問題,再來看看表達(dá)問題, 怎樣設(shè)計(jì)接口函數(shù)才能夠以最緊湊形式表達(dá)我們的意圖? 我們經(jīng)常需要面臨的實(shí)際問題:
A. 有多個(gè)元素要執(zhí)行類似的動(dòng)畫
B. 每個(gè)元素有多個(gè)屬性要同時(shí)變化
C. 執(zhí)行完一個(gè)動(dòng)畫之后開始另一個(gè)動(dòng)畫
jQuery對(duì)這些問題的解答可以說是榨盡了js語法表達(dá)力的最后一點(diǎn)剩余價(jià)值.
$('input')
.animate({left:'+=200px',top:'300'},2000)
.animate({left:'-=200px',top:20},1000)
.queue(function(){
// 這里dequeue將首先執(zhí)行隊(duì)列中的后一個(gè)函數(shù),因此alert("y")
$(this).dequeue();
alert('x');
})
.queue(function(){
alert("y");
// 如果不主動(dòng)dequeue, 隊(duì)列執(zhí)行就中斷了,不會(huì)自動(dòng)繼續(xù)下去.
$(this).dequeue();
});
A. 利用jQuery內(nèi)置的selector機(jī)制自然表達(dá)對(duì)一個(gè)集合的處理.
B. 使用Map表達(dá)多個(gè)屬性變化
C. 利用微格式表達(dá)領(lǐng)域特定的差量概念. '+=200px'表示在現(xiàn)有值的基礎(chǔ)上增加200px
D. 利用函數(shù)調(diào)用的順序自動(dòng)定義animation執(zhí)行的順序: 在后面追加到執(zhí)行隊(duì)列中的動(dòng)畫自然要等前面的動(dòng)畫完全執(zhí)行完畢之后再啟動(dòng).
jQuery動(dòng)畫隊(duì)列的實(shí)現(xiàn)細(xì)節(jié)大概如下所示,
A. animate函數(shù)實(shí)際是調(diào)用queue(function(){執(zhí)行結(jié)束時(shí)需要調(diào)用dequeue,否則不會(huì)驅(qū)動(dòng)下一個(gè)方法})
queue函數(shù)執(zhí)行時(shí), 如果是fx隊(duì)列, 并且當(dāng)前沒有正在運(yùn)行動(dòng)畫(如果連續(xù)調(diào)用兩次animate,第二次的執(zhí)行函數(shù)將在隊(duì)列中等待),則會(huì)自動(dòng)觸發(fā)dequeue操作, 驅(qū)動(dòng)隊(duì)列運(yùn)行.
如果是fx隊(duì)列, dequeue的時(shí)候會(huì)自動(dòng)在隊(duì)列頂端加入"inprogress"字符串,表示將要執(zhí)行的是動(dòng)畫.
B. 針對(duì)每一個(gè)屬性,創(chuàng)建一個(gè)jQuery.fx對(duì)象。然后調(diào)用fx.custom函數(shù)(相當(dāng)于start)來啟動(dòng)動(dòng)畫。
C. custom函數(shù)中將fx.step函數(shù)注冊(cè)到全局的timerFuncs中,然后試圖啟動(dòng)一個(gè)全局的timer.
timerId = setInterval( fx.tick, fx.interval );
D. 靜態(tài)的tick函數(shù)中將依次調(diào)用各個(gè)fx的step函數(shù)。step函數(shù)中通過easing計(jì)算屬性的當(dāng)前值,然后調(diào)用fx的update來更新屬性。
E. fx的step函數(shù)中判斷如果所有屬性變化都已完成,則調(diào)用dequeue來驅(qū)動(dòng)下一個(gè)方法。
很有意思的是, jQuery的實(shí)現(xiàn)代碼中明顯有很多是接力觸發(fā)代碼: 如果需要執(zhí)行下一個(gè)動(dòng)畫就取出執(zhí)行, 如果需要啟動(dòng)timer就啟動(dòng)timer等. 這是因?yàn)閖s程序是單線程的,真正的執(zhí)行路徑只有一條,為了保證執(zhí)行線索不中斷, 函數(shù)們不得不互相幫助一下. 可以想見, 如果程序內(nèi)部具有多個(gè)執(zhí)行引擎, 甚至無限多的執(zhí)行引擎, 那么程序的面貌就會(huì)發(fā)生本質(zhì)性的改變. 而在這種情形下, 遞歸相對(duì)于循環(huán)而言會(huì)成為更自然的描述.
9. promise模式:因果關(guān)系的識(shí)別
現(xiàn)實(shí)中,總有那么多時(shí)間線在獨(dú)立的演化著, 人與物在時(shí)空中交錯(cuò),卻沒有發(fā)生因果. 軟件中, 函數(shù)們?cè)谠创a中排著隊(duì), 難免會(huì)產(chǎn)生一些疑問, 憑什么排在前面的要先執(zhí)行? 難道沒有它就沒有我? 讓全宇宙喊著1,2,3齊步前進(jìn), 從上帝的角度看,大概是管理難度過大了, 于是便有了相對(duì)論. 如果相互之間沒有交換信息, 沒有產(chǎn)生相互依賴, 那么在某個(gè)坐標(biāo)系中順序發(fā)生的事件, 在另外一個(gè)坐標(biāo)系中看來, 就可能是顛倒順序的. 程序員依葫蘆畫瓢, 便發(fā)明了promise模式.
promise與future模式基本上是一回事,我們先來看一下java中熟悉的future模式.
futureResult = doSomething();
...
realResult = futureResult.get();
發(fā)出函數(shù)調(diào)用僅僅意味著一件事情發(fā)生過, 并不必然意味著調(diào)用者需要了解事情最終的結(jié)果. 函數(shù)立刻返回的只是一個(gè)將在未來兌現(xiàn)的承諾(Future類型), 實(shí)際上也就是某種句柄. 句柄被傳來傳去, 中間轉(zhuǎn)手的代碼對(duì)實(shí)際結(jié)果是什么,是否已經(jīng)返回漠不關(guān)心. 直到一段代碼需要依賴調(diào)用返回的結(jié)果, 因此它打開future, 查看了一下. 如果實(shí)際結(jié)果已經(jīng)返回, 則future.get()立刻返回實(shí)際結(jié)果, 否則將會(huì)阻塞當(dāng)前的執(zhí)行路徑, 直到結(jié)果返回為止. 此后再調(diào)用future.get()總是立刻返回, 因?yàn)橐蚬P(guān)系已經(jīng)被建立, [結(jié)果返回]這一事件必然在此之前發(fā)生, 不會(huì)再發(fā)生變化.
future模式一般是外部對(duì)象主動(dòng)查看future的返回值, 而promise模式則是由外部對(duì)象在promise上注冊(cè)回調(diào)函數(shù).
function getData(){
return $.get('/foo/').done(function(){
console.log('Fires after the AJAX request succeeds');
}).fail(function(){
console.log('Fires after the AJAX request fails');
});
}
function showDiv(){
var dfd = $.Deferred();
$('#foo').fadeIn( 1000, dfd.resolve );
return dfd.promise();
}
$.when( getData(), showDiv() )
.then(function( ajaxResult, ignoreResultFromShowDiv ){
console.log('Fires after BOTH showDiv() AND the AJAX request succeed!');
// 'ajaxResult' is the server’s response
});
jQuery引入Deferred結(jié)構(gòu), 根據(jù)promise模式對(duì)ajax, queue, document.ready等進(jìn)行了重構(gòu), 統(tǒng)一了異步執(zhí)行機(jī)制. then(onDone, onFail)將向promise中追加回調(diào)函數(shù), 如果調(diào)用成功完成(resolve), 則回調(diào)函數(shù)onDone將被執(zhí)行, 而如果調(diào)用失敗(reject), 則onFail將被執(zhí)行. when可以等待在多個(gè)promise對(duì)象上. promise巧妙的地方是異步執(zhí)行已經(jīng)開始之后甚至已經(jīng)結(jié)束之后,仍然可以注冊(cè)回調(diào)函數(shù)
someObj.done(callback).sendRequest() vs. someObj.sendRequest().done(callback)
callback函數(shù)在發(fā)出異步調(diào)用之前注冊(cè)或者在發(fā)出異步調(diào)用之后注冊(cè)是完全等價(jià)的, 這揭示出程序表達(dá)永遠(yuǎn)不是完全精確的, 總存在著內(nèi)在的變化維度. 如果能有效利用這一內(nèi)在的可變性, 則可以極大提升并發(fā)程序的性能.
promise模式的具體實(shí)現(xiàn)很簡(jiǎn)單. jQuery._Deferred定義了一個(gè)函數(shù)隊(duì)列,它的作用有以下幾點(diǎn):
A. 保存回調(diào)函數(shù)。
B. 在resolve或者reject的時(shí)刻把保存著的函數(shù)全部執(zhí)行掉。
C. 已經(jīng)執(zhí)行之后, 再增加的函數(shù)會(huì)被立刻執(zhí)行。
一些專門面向分布式計(jì)算或者并行計(jì)算的語言會(huì)在語言級(jí)別內(nèi)置promise模式, 比如E語言.
def carPromise := carMaker <- produce("Mercedes");
def temperaturePromise := carPromise <- getEngineTemperature()
...
when (temperaturePromise) -> done(temperature) {
println(`The temperature of the car engine is: $temperature`)
} catch e {
println(`Could not get engine temperature, error: $e`)
}
在E語言中, <-是eventually運(yùn)算符, 表示最終會(huì)執(zhí)行, 但不一定是現(xiàn)在. 而普通的car.moveTo(2,3)表示立刻執(zhí)行得到結(jié)果. 編譯器負(fù)責(zé)識(shí)別所有的promise依賴, 并自動(dòng)實(shí)現(xiàn)調(diào)度.
10. extend: 繼承不是必須的
js是基于原型的語言, 并沒有內(nèi)置的繼承機(jī)制, 這一直讓很多深受傳統(tǒng)面向?qū)ο蠼逃耐瑢W(xué)們耿耿于懷. 但繼承一定是必須的嗎? 它到底能夠給我們帶來什么? 最純樸的回答是: 代碼重用. 那么, 我們首先來分析一下繼承作為代碼重用手段的潛力.
曾經(jīng)有個(gè)概念叫做"多重繼承", 它是繼承概念的超級(jí)賽亞人版, 很遺憾后來被診斷為存在著先天缺陷, 以致于出現(xiàn)了一種對(duì)于繼承概念的解讀: 繼承就是"is a"關(guān)系, 一個(gè)派生對(duì)象"is a"很多基類, 必然會(huì)出現(xiàn)精神分裂, 所以多重繼承是不好的.
class A{ public: void f(){ f in A } }
class B{ public: void f(){ f in B } }
class D: public A, B{}
如果D類從A,B兩個(gè)基類繼承, 而A和B類中都實(shí)現(xiàn)了同一個(gè)函數(shù)f, 那么D類中的f到底是A中的f還是B中的f, 抑或是A中的f+B中的f呢? 這一困境的出現(xiàn)實(shí)際上源于D的基類A和B是并列關(guān)系, 它們滿足交換律和結(jié)合律, 畢竟,在概念層面上我們可能難以認(rèn)可兩個(gè)任意概念之間會(huì)出現(xiàn)從屬關(guān)系. 但如果我們放松一些概念層面的要求, 更多的從操作層面考慮一下代碼重用問題, 可以簡(jiǎn)單的認(rèn)為B在A的基礎(chǔ)上進(jìn)行操作, 那么就可以得到一個(gè)線性化的結(jié)果. 也就是說, 放棄A和B之間的交換律只保留結(jié)合律, extends A, B 與 extends B,A 會(huì)是兩個(gè)不同的結(jié)果, 不再存在詮釋上的二義性. scala語言中的所謂trait(特性)機(jī)制實(shí)際上采用的就是這一策略.
面向?qū)ο蠹夹g(shù)發(fā)明很久之后, 出現(xiàn)了所謂的面向方面編程(AOP), 它與OOP不同, 是代碼結(jié)構(gòu)空間中的定位與修改技術(shù). AOP的眼中只有類與方法, 不知道什么叫做意義. AOP也提供了一種類似多重繼承的代碼重用手段, 那就是mixin. 對(duì)象被看作是可以被打開,然后任意修改的Map, 一組成員變量與方法就被直接注射到對(duì)象體內(nèi), 直接改變了它的行為.
prototype.js庫(kù)引入了extend函數(shù),
Object.extend = function(destination, source) {
for (var property in source) {
destination[property] = source[property];
}
return destination;
}
就是Map之間的一個(gè)覆蓋運(yùn)算, 但很管用, 在jQuery庫(kù)中也得到了延用. 這個(gè)操作類似于mixin, 在jQuery中是代碼重用的主要技術(shù)手段---沒有繼承也沒什么大不了的.
11. 名稱映射: 一切都是數(shù)據(jù)
代碼好不好, 循環(huán)判斷必須少. 循環(huán)和判斷語句是程序的基本組成部分, 但是優(yōu)良的代碼庫(kù)中卻往往找不到它們的蹤影, 因?yàn)檫@些語句的交織會(huì)模糊系統(tǒng)的邏輯主線, 使我們的思想迷失在疲于奔命的代碼追蹤中. jQuery本身通過each, extend等函數(shù)已經(jīng)極大減少了對(duì)循環(huán)語句的需求, 對(duì)于判斷語句, 則主要是通過映射表來處理. 例如, jQuery的val()函數(shù)需要針對(duì)不同標(biāo)簽進(jìn)行不同的處理, 因此定義一個(gè)以tagName為key的函數(shù)映射表
valHooks: { option: {get:function(){}}}
這樣在程序中就不需要到處寫
if(elm.tagName == 'OPTION'){
return ...;
}else if(elm.tagName == 'TEXTAREA'){
return ...;
}
可以統(tǒng)一處理
(valHooks[elm.tagName.toLowerCase()] || defaultHandler).get(elm);
映射表將函數(shù)作為普通數(shù)據(jù)來管理, 在動(dòng)態(tài)語言中有著廣泛的應(yīng)用. 特別是, 對(duì)象本身就是函數(shù)和變量的容器, 可以被看作是映射表. jQuery中大量使用的一個(gè)技巧就是利用名稱映射來動(dòng)態(tài)生成代碼, 形成一種類似模板的機(jī)制. 例如為了實(shí)現(xiàn)myWidth和myHeight兩個(gè)非常類似的函數(shù), 我們不需要
jQuery.fn.myWidth = function(){
return parseInt(this.style.width,10) + 10;
}
jQuery.fn.myHeight = function(){
return parseInt(this.style.height,10) + 10;
}
而可以選擇動(dòng)態(tài)生成
jQuery.each(['Width','Height'],function(name){
jQuery.fn['my'+name] = function(){
return parseInt(this.style[name.toLowerCase()],10) + 10;
}
});
12. 插件機(jī)制:其實(shí)我很簡(jiǎn)單
jQuery所謂的插件其實(shí)就是$.fn上增加的函數(shù), 那這個(gè)fn是什么東西?
(function(window,undefined){
// 內(nèi)部又有一個(gè)包裝
var jQuery = (function() {
var jQuery = function( selector, context ) {
return new jQuery.fn.init( selector, context, rootjQuery );
}
....
// fn實(shí)際就是prototype的簡(jiǎn)寫
jQuery.fn = jQuery.prototype = {
constructor: jQuery,
init: function( selector, context, rootjQuery ) {... }
}
// 調(diào)用jQuery()就是相當(dāng)于new init(), 而init的prototype就是jQuery的prototype
jQuery.fn.init.prototype = jQuery.fn;
// 這里返回的jQuery對(duì)象只具備最基本的功能, 下面就是一系列的extend
return jQuery;
})();
...
// 將jQuery暴露為全局對(duì)象
window.jQuery = window.$ = jQuery;
})(window);
顯然, $.fn其實(shí)就是jQuery.prototype的簡(jiǎn)寫.
無狀態(tài)的插件僅僅就是一個(gè)函數(shù), 非常簡(jiǎn)單.
// 定義插件
(function($){
$.fn.hoverClass = function(c) {
return this.hover(
function() { $(this).toggleClass(c); }
);
};
})(jQuery);
// 使用插件
$('li').hoverClass('hover');
對(duì)于比較復(fù)雜的插件開發(fā), jQuery UI提供了一個(gè)widget工廠機(jī)制,
$.widget("ui.dialog", {
options: {
autoOpen: true,...
},
_create: function(){ ... },
_init: function() {
if ( this.options.autoOpen ) {
this.open();
}
},
_setOption: function(key, value){ ... }
destroy: function(){ ... }
});
調(diào)用 $('#dlg').dialog(options)時(shí), 實(shí)際執(zhí)行的代碼基本如下所示:
this.each(function() {
var instance = $.data( this, "dialog" );
if ( instance ) {
instance.option( options || {} )._init();
} else {
$.data( this, "dialog", new $.ui.dialog( options, this ) );
}
}
可以看出, 第一次調(diào)用$('#dlg').dialog()函數(shù)時(shí)會(huì)創(chuàng)建窗口對(duì)象實(shí)例,并保存在data中, 此時(shí)會(huì)調(diào)用_create()和_init()函數(shù), 而如果不是第一次調(diào)用, 則是在已經(jīng)存在的對(duì)象實(shí)例上調(diào)用_init()方法. 多次調(diào)用$('#dlg').dialog()并不會(huì)創(chuàng)建多個(gè)實(shí)例.
13. browser sniffer vs. feature detection
瀏覽器嗅探(browser sniffer)曾經(jīng)是很流行的技術(shù), 比如早期的jQuery中
jQuery.browser = {
version:(userAgent.match(/.+(?:rv|it|ra|ie)[/: ]([d.]+)/) || [0,'0'])[1],
safari:/webkit/.test(userAgent),
opera:/opera/.test(userAgent),
msie:/msie/.test(userAgent) && !/opera/.test(userAgent),
mozilla:/mozilla/.test(userAgent) && !/(compatible|webkit)/.test(userAgent)
};
在具體代碼中可以針對(duì)不同的瀏覽器作出不同的處理
if($.browser.msie) {
// do something
} else if($.browser.opera) {
// ...
}
但是隨著瀏覽器市場(chǎng)的競(jìng)爭(zhēng)升級(jí), 競(jìng)爭(zhēng)對(duì)手之間的互相模仿和偽裝導(dǎo)致userAgent一片混亂, 加上Chrome的誕生, Safari的崛起, IE也開始加速向標(biāo)準(zhǔn)靠攏, sniffer已經(jīng)起不到積極的作用. 特性檢測(cè)(feature detection)作為更細(xì)粒度, 更具體的檢測(cè)手段, 逐漸成為處理瀏覽器兼容性的主流方式.
jQuery.support = {
// IE strips leading whitespace when .innerHTML is used
leadingWhitespace: ( div.firstChild.nodeType === 3 ),
...
}
只基于實(shí)際看見的,而不是曾經(jīng)知道的, 這樣更容易做到兼容未來.
14. Prototype vs. jQuery
prototype.js是一個(gè)立意高遠(yuǎn)的庫(kù), 它的目標(biāo)是提供一種新的使用體驗(yàn),參照Ruby從語言級(jí)別對(duì)javascript進(jìn)行改造,并最終真的極大改變了js的面貌。$, extends, each, bind...這些耳熟能詳?shù)母拍疃际莗rototype.js引入到j(luò)s領(lǐng)域的. 它肆無忌憚的在window全局名字空間中增加各種概念, 大有誰先占坑誰有理, 舍我其誰的氣勢(shì). 而jQuery則扣扣索索, 抱著比較實(shí)用化的理念, 目標(biāo)僅僅是write less, do more而已.
不過等待激進(jìn)的理想主義者的命運(yùn)往往都是壯志未酬身先死. 當(dāng)prototype.js標(biāo)志性的bind函數(shù)等被吸收到ECMAScript標(biāo)準(zhǔn)中時(shí), 便注定了它的沒落. 到處修改原生對(duì)象的prototype, 這是prototype.js的獨(dú)門秘技, 也是它的死穴. 特別是當(dāng)它試圖模仿jQuery, 通過Element.extend(element)返回增強(qiáng)對(duì)象的時(shí)候, 算是徹底被jQuery給帶到溝里去了. prototype.js與jQuery不同, 它總是直接修改原生對(duì)象的prototype, 而瀏覽器卻是充滿bug, 謊言, 歷史包袱并夾雜著商業(yè)陰謀的領(lǐng)域, 在原生對(duì)象層面解決問題注定是一場(chǎng)悲劇. 性能問題, 名字沖突, 兼容性問題等等都是一個(gè)幫助庫(kù)的能力所無法解決的. Prototype.js的2.0版本據(jù)說要做大的變革, 不知是要與歷史決裂, 放棄兼容性, 還是繼續(xù)掙扎, 在夾縫中求生.