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)

元素element

1

屬性attr

2

文本text

3

註釋comments

8

文檔document

9

jQuery的"棧"

  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 duplicates

53             if ( !guaranteedUnique[ name ] ) {

54                 jQuery.unique( matched );

55             }

56 

57             // Reverse order for parents* and prev-derivatives

58             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 set

11         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 duplicates

53             if ( !guaranteedUnique[ name ] ) {

54                 jQuery.unique( matched );

55             }

56 

57             // Reverse order for parents* and prev-derivatives

58             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吧!

 

 

發佈留言