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