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