jQuery 源碼分析和使用心得 - 文檔遍曆 ( traversing.js )
jQuery之所以這麼好用, 首先一點就是$()方法和它強大的選取器. 其中選取器使用的是sizzle引擎, sizzle是jQuery的子項目, 提供高效的選取器查詢. 有個好訊息告訴大家, 就是sizzle可以獨立使用, 如果你覺得jQuery太大但又非常喜歡它的選取器, 那不妨可以用sizzle. 感興趣的話可以到官方網站瞭解. 本系列內部不準備解析sizzle的源碼, 一是sizzle內容相對獨立, 二是內容主要涉及演算法, 與整體的代碼設計關係不大, 三嘛, 我的實力有限, 遇到演算法就退縮了,哈哈! ( q君: 這才是主要原因吧! ). 不過也許以後技術水平到了, 出一個sizzle解析專題也未可知啊! 回過神來, 看我們的標題就知道了, jQuery這麼強大, 它的眾多方便的遍曆方法也是一大功臣啊 . jQuery提供了十幾種方便的鏈式遍曆方法, 讓我們可以在繁雜的dom結構中自由遊走, 這一章裡我們就來一探究竟, 看看裡面到底蘊含了怎麼樣奇妙的實現原理呢! 預熱DOM樹 要說遍曆, 首先要介紹"樹" ,一些沒有看過資料結構或者不瞭解html dom結構的人可能對樹沒有什麼概念, 如果你已經知道了, 就跳過本段吧. 我簡單的說明一下, 具體定義和非常正規的說明我就不說了,相信度娘一定可以滿足你的. 我們先來想象一下一顆樹, 他有根, 然後分叉出大的枝幹, 然後就分出小樹枝 ... 最後到葉子結束. 如果我們把根, 枝幹, 小樹枝, 葉子 抽象成節點, 他們之間存在串連, 這些節點和串連就組成了樹. 應用到html中就是如下(來自百度圖片搜尋) dom樹 樹根就是document, 到html元素, 然後分叉 ... 一直到最後的文本. 所以說html整個就是一顆樹. 樹中的所有節點都直接或間接的連通, 而且可以看到屬性結構不存在環狀的串連. 樹的遍曆就是通過document和下面的所有節點, 通過他們的串連在各個節點上遊走, 訪問上面的資料. jQuery比較常用的幾種遍曆文檔的方法有 parent parents children siblings next prev等等. DOM屬性 在解析遍曆源碼之前, 還要普及幾點dom的幾個屬性和方法. 一般我們通過document.getElementByXX的這種方法就可獲得dom節點和dom節點的數組. dom中包含了非常多的屬性, 包括父節點, 子節點 , 相鄰節點的引用, 自身的一些數值或者位置, 大小等資訊. jQuery的遍曆方法也是基於這些屬性實現的. 有一點需要介紹的是 nodeType屬性, nodeType標記了當前節點的類型. dom節點比較重要的幾個是(來自百度百科) 元素節點節點類型取值(nodeType)元素element1屬性attr2文本text3注釋comments8文檔document9jQuery的"棧" jQuery的鏈式尋找是非常舒服的, 比如尋找某個列表下的連結可以用 $("#some-list").children("li").find("a"), 這裡我為什麼要用多次尋找呢, 因為跟jQuery的"棧"有關嘛. 我們先來看看執行children和find的時候做了什麼. 1 find: function( selector ) { 2 var i, 3 len = this.length, 4 ret = [], 5 self = this; 6 7 if ( typeof selector !== "string" ) { 8 return this.pushStack( jQuery( selector ).filter(function() { 9 for ( i = 0; i < len; i++ ) {10 if ( jQuery.contains( self[ i ], this ) ) {11 return true;12 }13 }14 }) );15 }16 17 for ( i = 0; i < len; i++ ) {18 jQuery.find( selector, self[ i ], ret );19 }20 21 // Needed because $( selector, context ) becomes $( context ).find( selector )22 ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret );23 ret.selector = this.selector ? this.selector + " " + selector : selector;24 return ret;25 }複製代碼 複製代碼 1 jQuery.each({ 2 parent: function( elem ) { 3 var parent = elem.parentNode; 4 return parent && parent.nodeType !== 11 ? parent : null; 5 }, 6 parents: function( elem ) { 7 return jQuery.dir( elem, "parentNode" ); 8 }, 9 parentsUntil: function( elem, i, until ) {10 return jQuery.dir( elem, "parentNode", until );11 },12 next: function( elem ) {13 return sibling( elem, "nextSibling" );14 },15 prev: function( elem ) {16 return sibling( elem, "previousSibling" );17 },18 nextAll: function( elem ) {19 return jQuery.dir( elem, "nextSibling" );20 },21 prevAll: function( elem ) {22 return jQuery.dir( elem, "previousSibling" );23 },24 nextUntil: function( elem, i, until ) {25 return jQuery.dir( elem, "nextSibling", until );26 },27 prevUntil: function( elem, i, until ) {28 return jQuery.dir( elem, "previousSibling", until );29 },30 siblings: function( elem ) {31 return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem );32 },33 children: function( elem ) {34 return jQuery.sibling( elem.firstChild );35 },36 contents: function( elem ) {37 return elem.contentDocument || jQuery.merge( [], elem.childNodes );38 }39 }, function( name, fn ) {40 jQuery.fn[ name ] = function( until, selector ) {41 var matched = jQuery.map( this, fn, until );42 43 if ( name.slice( -5 ) !== "Until" ) {44 selector = until;45 }46 47 if ( selector && typeof selector === "string" ) {48 matched = jQuery.filter( selector, matched );49 }50 51 if ( this.length > 1 ) {52 // Remove duplicates53 if ( !guaranteedUnique[ name ] ) {54 jQuery.unique( matched );55 }56 57 // Reverse order for parents* and prev-derivatives58 if ( rparentsprev.test( name ) ) {59 matched.reverse();60 }61 }62 63 return this.pushStack( matched );64 };65 });複製代碼 這兩種函數都在最後調用了this.pushStack 複製代碼 1 pushStack: function( elems ) { 2 3 // Build a new jQuery matched element set 4 var ret = jQuery.merge( this.constructor(), elems ); 5 6 // Add the old object onto the stack (as a reference) 7 ret.prevObject = this; 8 ret.context = this.context; 9 10 // Return the newly-formed element set11 return ret;12 } 複製代碼 這個函數在第7行中將this賦值給了新對象的prevObject屬性, 也就是說, 我們在每次通過已有的jQuery對象調用find或者children, parent...進行尋找的時候都會把原來的儲存在新對象中, 這樣就提供了一個可回退的尋找棧. 那麼當我們使用$("#some-list").children("li").find("a")這種方式進行尋找的時候, 可以從後面的結果中回溯到上一次尋找的結果, 示範樣本. 基本遍曆 jQuery的遍曆思路很簡單. 它先提供了兩個基本的遍曆函數, 一個是dir, 一個是sibling , 然後建立快捷的遍曆方法調用基本遍曆函數, 再經過後續的去重封裝成jQuery, 最後壓棧返回結果.(q君: 資訊量好大, 看完下面詳細解說再看這個流程就好懂了 ) 基本遍曆: dir sibling dir有三個參數, function( elem, dir, until ), elem是dom對象, dir是需要遍曆的屬性, until是截至條件. 運行過程是迴圈尋找elem的dir的屬性, 直到沒有後續元素 或者找到了document根節點(elem.nodeType !== 9) , 將所有尋找到的元素放到數組中返回 . sibling有兩個參數, function( n, elem ) , n是起始dom對象, elem是結束dom對象. 它通過不斷尋找nextSibling, 直到找到非element的對象(n.nodeType === 1) 或者找到了elem為止, 將所有尋找到的元素放到數組中返回 . 另外還有一個基本遍曆方法sibling, 這個方法並沒有對外公開. 它尋找dir屬性直到遇到第一個element的對象或者沒找到, 並返回這個對象. 1 function sibling( cur, dir ) {2 while ( (cur = cur[dir]) && cur.nodeType !== 1 ) {}3 return cur;4 } 捷徑 jQuery提供 parent , parents , next 等十幾種遍曆方法, 這些方法都是以三個基本遍曆方法為基礎實現, 這段代碼看起來非常優雅, 我忍不住要再貼一遍, 雖然上面已經有了. 複製代碼 1 jQuery.each({ 2 parent: function( elem ) { 3 var parent = elem.parentNode; 4 return parent && parent.nodeType !== 11 ? parent : null; 5 }, 6 parents: function( elem ) { 7 return jQuery.dir( elem, "parentNode" ); 8 }, 9 parentsUntil: function( elem, i, until ) {10 return jQuery.dir( elem, "parentNode", until );11 },12 next: function( elem ) {13 return sibling( elem, "nextSibling" );14 },15 prev: function( elem ) {16 return sibling( elem, "previousSibling" );17 },18 nextAll: function( elem ) {19 return jQuery.dir( elem, "nextSibling" );20 },21 prevAll: function( elem ) {22 return jQuery.dir( elem, "previousSibling" );23 },24 nextUntil: function( elem, i, until ) {25 return jQuery.dir( elem, "nextSibling", until );26 },27 prevUntil: function( elem, i, until ) {28 return jQuery.dir( elem, "previousSibling", until );29 },30 siblings: function( elem ) {31 return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem );32 },33 children: function( elem ) {34 return jQuery.sibling( elem.firstChild );35 },36 contents: function( elem ) {37 return elem.contentDocument || jQuery.merge( [], elem.childNodes );38 }39 }, function( name, fn ) {40 jQuery.fn[ name ] = function( until, selector ) {41 var matched = jQuery.map( this, fn, until );42 43 if ( name.slice( -5 ) !== "Until" ) {44 selector = until;45 }46 47 if ( selector && typeof selector === "string" ) {48 matched = jQuery.filter( selector, matched );49 }50 51 if ( this.length > 1 ) {52 // Remove duplicates53 if ( !guaranteedUnique[ name ] ) {54 jQuery.unique( matched );55 }56 57 // Reverse order for parents* and prev-derivatives58 if ( rparentsprev.test( name ) ) {59 matched.reverse();60 }61 }62 63 return this.pushStack( matched );64 };65 });複製代碼 當我第一次看見這段代碼的時候, 不禁感歎js真是太靈活了, 而jQuery的開發人員將這種靈活性發揮的淋漓盡致. 整段代碼前半部分看起來就像是一個配置. 函數名, 後面是方法的實現. 比如parents, 他調用dir方法, 傳入當前elem和遍曆屬性"parentNode", 這個方法就會不斷訪問元素的parentNode屬性尋找父級元素, 一直查到 document位置, 返回的就是當前元素的所有父級元素. 再看後半部分, jQuery.map( this, fn, until ) 遍曆本身, 對每一個元素執行fn方法, 傳入until參數. 返回的就是所有遍曆後得到的元素(dom元素, 可能會有重複). jQuery.filter( selector, matched )對元素進行過濾, 然後去重, 如果是parent, prev等方法, 就將結果反轉順序, 最後壓棧返回. 使用建議 1. 通過prevObject可以擷取上一次尋找結果 2. 先提供基本方法, 然後建立捷徑的做法可以在以後的代碼中借鑒 3. 感歎jQuery吧!