基于jquery-1.4.3rc1版本的. 正式版據(jù)說過幾天就發(fā)布, 應(yīng)該差別不大.
這個系列應(yīng)該有十章, 本來準(zhǔn)備寫完了一起發(fā)的. 但有幾章還不知道會拖到什么時候. 現(xiàn)在完成大概五章的內(nèi)容了. 會陸續(xù)放上來. 這個源碼分析系列我不想它成為單純的翻譯注釋.除了那種一眼就明白的代碼, 其它基本都加了注解. 有時候一句代碼的分析可能會關(guān)聯(lián)到一個重要的知識點, 我也盡量在能力范圍內(nèi)把它講清. 看源碼前盡可能的多思考一下,假如是我們實現(xiàn)這個方法,會怎么去做. 再對比jquery的實現(xiàn)就更能加深理解. 另外你也可以對jquery的某些代碼保持懷疑, 每次版本更新,都會修復(fù)很多bug, 可能你現(xiàn)在懷疑的就是其中一個. 這些源碼分析里面的錯誤和缺陷肯定是有的. 寫成文檔的目的一是為了梳理自己的知識,再就是希望在和大家的討論中, 能認識和改正自己的錯誤. 最后面提供了pdf下載. ------------------------------- 分割線----------------------------------------------- jquery核心 一 構(gòu)造jquery. 相對于其它庫里傳統(tǒng)的構(gòu)造對象方法. jquery提供了一種截然不同的方法. 它選擇創(chuàng)造一個全新的奇異世界. 首先所有的jquery代碼被一個自動執(zhí)行的閉包包裹起來, 只在后面暴露$和jQuery這2個變量給外界 盡量避開變量沖突.
window和undefined都是為了減少變量查找所經(jīng)過的scope. 當(dāng)window通過傳遞給閉包內(nèi)部之后, 在閉包內(nèi)部使用它的時候, 可以把它當(dāng)成一個局部變量, 顯然比原先在window scope下查找的時候要快一些. undefined也是同樣的道理, 其實這個undefined并不是javascript數(shù)據(jù)類型六君子之一的undefined, 而是一個普普通通的變量名. 只是因為沒給它傳遞值. 它的值就是undefined. undefined并不是javascript的保留字. 然后是一個套子套住jquery的構(gòu)造方法
首先定義jq1, 這個jQuery最終在return (window.jQuery = window.$ = jQuery)的時候會變成 window下面的變量供外界使用. 而jq2供jquery內(nèi)部執(zhí)行的時候調(diào)用. 最終作為jq1的引用返回. return (window.jQuery = window.$ = jQuery);這句話等價于
現(xiàn)在來看看jquery對象是怎么被創(chuàng)建出來的. jquery作為一個獨立特行的庫, 它產(chǎn)生jquery對象時并不需要用new 操作符.. 它寧愿選擇這種方式, 比如要產(chǎn)生一個構(gòu)造函數(shù)Man的對象.
同樣真正作為jQuery對象的構(gòu)造方法的并不是 function (selector, context){ } 而是jQuery.fn.init.
jQuery.fn就是jQuery.prototype. 見源碼102行. jQuery.fn = jQuery.prototype = {} init是掛在jQuery.prototype上的屬性. 當(dāng)jQuery(‘div’)的時候, 實際上轉(zhuǎn)交給了jQuery.fn.init構(gòu)造函數(shù)來生成對象. 當(dāng)然我們想用new jQuery來生成jquery對象也可以. 跟直接用jQuery()沒區(qū)別. 因為構(gòu)造函數(shù)一定會返回一個對象.如果顯示指定了返回某個對象.就會返回那個對象, 否則才會返回this對象. 好比說, 有只母雞被你強迫下一個蛋, 它會先看窩里有沒有別人的蛋, 如果沒有,才會自己努力下一個. 這里顯然返回的是jQuery.fn.init的對象. 也許現(xiàn)在你開始回憶制作jquery插件時, 明明是給jQuery.prototype添加方法. 這里返回的又是jQuery.prototype.init的對象. 原來在源碼333行, jQuery.prototype.init.prototype = jQuery. prototype; 現(xiàn)在很容易看明白. 給jQuery.prototype添加方法就等于給jQuery. prototype.init.prototype添加方法了. JQuery api里的方法大部分都是通過jQuery.prototype擴展上去的, 除此之外. 我們還要給jquery對象加上索引. 給集合添加length屬性,讓他們更像一個數(shù)組里的元素. 搞明白這些, 再來看jQuery. prototype.init這個方法里究竟是怎樣生產(chǎn)jquery對象的. 我們可以把jQuery. prototype.init想象成一個火腿腸加工器. 只要你放了正確的原料進去, 它就可以把原料變成火腿腸生產(chǎn)出來.如果你不小心放錯了原料.它也會幫你變成火腿腸. 不過只有塑料包裝, 里面沒有火腿. 當(dāng)然這個加工器里面的構(gòu)造是很復(fù)雜的, 它需要判斷材料種類, 數(shù)量等等. 一般這個材料主要為這4種情況 1 dom節(jié)點 2 字符串 3 函數(shù) 4 數(shù)組 5 其他元素 一 jQuery構(gòu)造方法 jQuery的構(gòu)造方法會生成一組jquery對象的集合.具體關(guān)于init方法的分析, 還是留在選擇器部分說吧. 二 jQuery對象訪問 jquery構(gòu)造完對象之后, 會提供一些方法訪問這些對象. 1 jQuery.prototype.size 集合內(nèi)元素的數(shù)量 就是通過this.length得到. 2 jQuery.prototype.get 按照索引取得集合內(nèi)某個元素, 返回包裝前的原始對象
很簡單, 就是讓當(dāng)前jquery對象冒充Array的對象, 調(diào)用Array.prototype.slice進行截斷. 返回的是一個數(shù)組. 至于為什么可以像這樣使用對象冒充. 我們抽個地方來好好討論一下. 其實如果查看v8之類開源引擎的源碼就知道(當(dāng)然也可以在ecma里掙扎一番). 要調(diào)用Array原型鏈上的方法. 通常這個對象滿足2個條件就可以了. 1, 本身可以存取屬性. 2, length屬性不能是只讀(可以沒有l(wèi)ength屬性). 由于Array.prototype.slice方法太長了. 我拿Array.prototype.push方法舉例. 在V8的src目錄下的array.js可以找到這些方法. 比如push
可以看到push操作的核心就是復(fù)制屬性和重設(shè)長度. jquery對象完全可以滿足這2個條件. 同樣的道理 一個對象字面量{}也可以. 而string類型的不可以, 因為不能在string上存取屬性. function對象雖然可以存取屬性, 也有l(wèi)ength屬性. 不過它的length屬性比較特殊, 表示形參的個數(shù), 是一個只讀屬性, 源碼中的this.length = n + m這一句不起作用, 所以function對象也不行. 同理window對象也不行. 上面的slice.call也是這個原理, 雖然slice方法的實現(xiàn)更復(fù)雜一點. 明白了這個,我們可以解釋很多奇怪的問題.比如:
如果在push操作之前添加一句 a.length = 2; 再進行push操作后, a.length就為3了. 3 jQuery.prototype.index 搜索匹配的元素,并返回相應(yīng)元素的索引值,從0開始計數(shù)。 如果不給 .index() 方法傳遞參數(shù),那么返回值就是這個jQuery對象集合中第一個元素相對于其同輩元素的位置。 如果參數(shù)是一組DOM元素或者jQuery對象,那么返回值就是傳遞的元素相對于原先集合的位置。 如果參數(shù)是一個選擇器,那么返回值就是原先元素相對于選擇器匹配元素中的位置。如果找不到匹配的元素,則返回-1。 沒有什么特別需要解釋的, 直接看代碼.
顧名思義jQuery.inArray就是判斷數(shù)組里有沒有某個元素.當(dāng)然這里的數(shù)組也包括偽數(shù)組. 這個方法雖然實現(xiàn)起來很簡單, 關(guān)于inArray這個名字在jquery的官方論壇卻有頗多爭議. 很多人認為它應(yīng)該返回true或者false, 而不是索引的位置. john resig只是說暫時還不準(zhǔn)備修改這個方法. 有些瀏覽器還不支持Array.prototype.indexOf方法. 所以首先在源碼的851行, 有這樣一段代碼.
如果支持Array.prototype.indexOf. 則重寫jQuery.inArray, 直接用Array.prototype.indexOf.call(array, elem ); 在頁面加載的時候就重寫這個方法. 也避免了在函數(shù)里反復(fù)判斷造成的浪費. 然后
三 數(shù)據(jù)緩存 jQuery.data 在實際應(yīng)用中, 我們經(jīng)常需要往節(jié)點中緩存一些數(shù)據(jù). 這些數(shù)據(jù)往往和dom元素緊密相關(guān). dom節(jié)點也是對象, 所以我們可以直接擴展dom節(jié)點的屬性. 不過肆意污染dom節(jié)點是不良少年的行為. 我們需要一種低耦合的方式讓dom和緩存數(shù)據(jù)能夠聯(lián)系起來. jquery提供了一套非常巧妙的緩存辦法. 我們先在jquery內(nèi)部創(chuàng)建一個cache對象{}, 來保存緩存數(shù)據(jù). 然后往需要進行緩存的dom節(jié)點上擴展一個值為jQuery.expando的屬性, 這里是”jquery” + (new Date).getTime(). 接著把每個節(jié)點的dom[jQuery.expando]的值都設(shè)為一個自增的變量id,保持全局唯一性. 這個id的值就作為cache的key用來關(guān)聯(lián)dom節(jié)點和數(shù)據(jù). 也就是說cache[id]就取到了這個節(jié)點上的所有緩存. 而每個元素的所有緩存都被放到了一個map里面,這樣可以同時緩存多個數(shù)據(jù). 比如有2個節(jié)點dom1和dom2, 它們的緩存數(shù)據(jù)在cache中的格式應(yīng)該是這樣
jQuery.expando的值等于”jquery”+當(dāng)前時間, 元素本身具有這種屬性而起沖突的情況是微乎其微的. 我們在看源碼之前, 先根據(jù)上面的原理來自己實現(xiàn)一個簡單的緩存系統(tǒng).以便增強理解. 先把跟data相關(guān)的所有代碼都封裝到一個閉包里,通過返回的接口暴露給外界. 同時為了簡便,我們拆分成setData和getData兩個方法.
看看源碼實現(xiàn). 首先聲明一些特殊的節(jié)點, 在它們身上存屬性的時候可能會拋出異常.
這個對象里的數(shù)據(jù)用在acceptData方法中, 跟1.42版本相比, 這里多了對什么flash的object的特殊處理. 總之a(chǎn)cceptData方法就是判斷節(jié)點能否添加緩存. 看具體的jQuery.data
從新版本的源碼里可以看到, 1.42版本中data方法的幾個缺點已經(jīng)被解決了. 當(dāng)然我們用jquery緩存系統(tǒng)的時候, 一般調(diào)用的是prototype方法, prototype方法除了調(diào)用上面的靜態(tài)方法之外. 還加入了對節(jié)點上自定義事件的處理, 留在event部分再講. 當(dāng)然, 我們還需要刪除緩存的方法. 現(xiàn)在看看removeData的代碼
四 隊列控制 隊列控制也是jquery中很有特點的一個功能. 可以用來管理動畫或者事件等的執(zhí)行順序. queue和dequeue主要為動畫服務(wù). 比如在jquery的動畫里, 因為javascript的單線程異步機制, 如果要管理一批動畫的執(zhí)行順序, 而不是讓它們一起在屏幕上飛舞. 一般我們是一個一個的把下個動畫寫在上個動畫的回調(diào)函數(shù)中, 意味著如果要讓十個動畫按次序執(zhí)行. 至少要寫9個回調(diào)函數(shù). 好吧我承認我做過這樣的事, 直接導(dǎo)致我的視力從5.2變?yōu)?.1. 現(xiàn)在有了隊列機制, 可以把動畫都放到隊列中依次執(zhí)行.究竟怎樣把動畫填充進隊列.用的是queue方法. 不過queue非常先進的是.把動畫push進隊列之后,還會自動去執(zhí)行隊列里的第一個函數(shù). 隊列機制里面的另外一個重要方法是dequeue, 意為取出隊列的第一個函數(shù)并執(zhí)行.此時隊列里面的函數(shù)個數(shù)變?yōu)镹-1. 看個例子.比如我要讓2個div以動畫效果交替隱藏和顯示.同時只能有一個div在進行動畫.
首先我們需要一個載體, 來保存這個隊列, 這里選擇了document. 其實選什么節(jié)點都一樣, 保存隊列其實也是一個jQuery.data操作. 然后queue的參數(shù)是一個數(shù)組. 里面的函數(shù)就是隊列依次執(zhí)行的函數(shù). 前面講到, queue方法會自動把隊列里的第一個函數(shù)取出來執(zhí)行. 意味著這些代碼寫完后, div1已經(jīng)開始漸漸隱藏了. 隱藏完畢后, 如果要讓后面的動畫繼續(xù)執(zhí)行, 還要用$(document).dequeue()繼續(xù)取出并執(zhí)行現(xiàn)在隊列里的第一個函數(shù). 當(dāng)然這個操作是放在第一個動畫的回調(diào)函數(shù)里, 以此類推, 第二個.dequeue()要放在第二個動畫的回調(diào)函數(shù)里. 我們看到這里沒有用$(document).dequeue(). 因為這句代碼太長. 注意隊列函數(shù)里有一個參數(shù)fn, fn是dequeue方法內(nèi)部傳給此函數(shù)的, 就是$(document).dequeue(). 在看源碼之前, 先自己來想想這個功能應(yīng)該怎么實現(xiàn). 首先我們需要一個數(shù)組, 里面存放那些動畫. 然后需要2個方法, set和get.可以存取動畫. 可能我們還需要一個變量來模擬線程鎖, 保證隊列里的函數(shù)不會同時被執(zhí)行. 最后我們要把這個數(shù)組存入dom的緩存中, 方便隨時存取和刪除. 看源碼, 先是prototype方法.
“inprogress”進程鎖是這樣工作的: 如果是dequeue操作, 去掉鎖, 執(zhí)行隊列里的函數(shù), 同時給隊列加上鎖. 如果是queue操作, 要看鎖的狀態(tài), 如果被鎖上了, 就只執(zhí)行隊列的添加操作. 不再調(diào)用dequeue. 其實dequeue和queue都可以執(zhí)行隊列里的第一個函數(shù).queue操作添加完隊列之后, 會調(diào)用dequeue方法去執(zhí)行函數(shù). 但是用dequeue執(zhí)行函數(shù)的時候, 這時候如果又用queue觸發(fā)dequeue的話, 很可能同時有2個函數(shù)在執(zhí)行. 隊列就失去一大半意義了(還是可以保證順序, 但是2個動畫會同時執(zhí)行). 不過這個鎖只能保證在dequeue的時候, 不被queue操作意外的破壞隊列. 如果人為的同時用2個dequeue, 還是會破壞動畫效果的. 所以要把fn寫在回調(diào)函數(shù)里. 清空隊列
原理上面已經(jīng)提到過了, 就是這一句
用一個空數(shù)組代替了原來的隊列. 五 多庫共存 jQuery.noConflict 將變量$的控制權(quán)讓渡給上一個實現(xiàn)它的那個庫. 可能很多人都被多庫共存時的$變量問題困擾過. 比如你先引入了prototype庫,然后又引入了jquery.因為jquery的$會覆蓋prototype的$. 現(xiàn)在想調(diào)用prototype的$怎么辦呢, noConflict方法就派上用場了. Jquery代碼里最開始就有一句_$ = window.$ , 在加載的時候就用_$來引用原來的$ (比如現(xiàn)在就是prototype庫里的$).
繼承和拷貝 jQuery.prototype.extend和jQuery.extend 擴展 jQuery 元素集來提供新的方法, 或者把一個對象的屬性拷貝到另外一個對象上. 通常javascript的繼承實現(xiàn)有4種方式. 1 構(gòu)造繼承 2 prototype繼承 3 屬性拷貝 4 實例繼承(這個有點特殊, 主要用于繼承內(nèi)置對象) 這4種繼承都有各自的優(yōu)點和缺陷. 構(gòu)造繼承查找屬性快.但無法繼承原型鏈上的方法,而且每次都要為屬性復(fù)制一份新的副本進行儲存 原型繼承在性能方面好一些,可以共享原型鏈.但查找屬性慢,因為可能要遍歷N條原型鏈才能找到某個屬性.而且原型鏈太多,會使得結(jié)構(gòu)越加混亂.并且會丟失對象的constructor屬性 (對象的constructor總是指向原型鏈最上層的構(gòu)造器) 屬性拷貝非常靈活,但明顯效率偏低. 而且僅僅只是模擬繼承. 實例繼承主要用來繼承Array, Date等內(nèi)置對象, 用的較少. jquery里采用的是屬性拷貝.其實用哪種繼承是個見仁見智的問題. 也有其它一些庫就是用的原型繼承. 前面也可以注意到, jquery只在構(gòu)造方法或者原型鏈上定義了少量的核心方法. 其它功能塊都是通過extend函數(shù)拷貝上去.按需定制. 好比開發(fā)商交給我們房子的時候, 只安裝了水電暖氣等基本設(shè)施.電視機洗衣機顯然是自己去買比較好. 屬性拷貝原理就是遍歷第二個對象, 然后分別把鍵和值填入第一個對象中.類似于
不幸的是jquery里面的做法復(fù)雜的多,不僅可以把多個參數(shù)的屬性都復(fù)制到一個對象中,還能實現(xiàn)深度繼承. 深度繼承是指如果被繼承對象和繼承對象都有同名屬性a, 而且這個屬性的值分別是一個對象b和c.會把b和c的屬性都合并到一個新的對象里, 作為值返回給合并后的對象. 說的有點復(fù)雜, 看個例子.
jQuery的做法主要是這樣, 先修正參數(shù),根據(jù)第一個參數(shù)是boolean還是object確定target是哪個對象. 如果有且只有一個object類型的參數(shù), 則把這個參數(shù)里的屬性copy到j(luò)Query或者jQuery.prototype上, 擴展jQuery對象.用這個辦法可以很方便的編寫插件. 否則依次遍歷第一個object參數(shù)之后的所有object類型對象的屬性. 根據(jù)是否深度繼承用不同的方式進行copy操作. extend會改變第一個參數(shù)的引用. 如果不想改變它,可以稍微變通一下,把所有的參數(shù)屬性都拷貝到一個空對象上.如$.extend({}, obj1, obj2);
[/size][/size] |
|
來自: quasiceo > 《javascript》