jQuery 源碼分析 7: sizzle

  jQuery使用的是sizzle這個選擇器引擎,這個引擎以其高速著稱,其實現十分精妙可是也足夠複雜,下面現簡單分析一下相關的代碼。node

 

在jQuery的部分API接口是直接引用了Sizzle的方法,這些接口以下:web

1 jQuery.find = Sizzle;
2 jQuery.expr = Sizzle.selectors;
3 jQuery.expr[":"] = jQuery.expr.pseudos;
4 jQuery.unique = Sizzle.uniqueSort;
5 jQuery.text = Sizzle.getText;
6 jQuery.isXMLDoc = Sizzle.isXML;
7 jQuery.contains = Sizzle.contains;

 

  jQuery.find 引用的就是Sizzle,下面看看Sizzle的實現
 
  1  // @param selector 已去掉頭尾空白的選擇器字符串 
  2  // @param context 執行匹配的最初的上下文(即DOM元素集合)。若context沒有賦值,則取document。 
  3  // @param results 已匹配出的部分最終結果。若results沒有賦值,則賦予空數組。 
  4  // @param seed 初始集合 
  5 
  6 function Sizzle( selector, context, results, seed ) {
  7      var match, elem, m, nodeType,
  8           // QSA vars
  9           i, groups, old, nid, newContext, newSelector;
 10       if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
 11 
 12           // 根據不一樣的瀏覽器環境,設置合適的Expr方法,構造合適的rbuggy測試
 13           setDocument( context );
 14      }
 15       context = context || document;
 16      results = results || [];
 17      nodeType = context.nodeType;
 18       if ( typeof selector !== "string" || !selector ||
 19           nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {
 20            return results;
 21      }
 22       if ( !seed && documentIsHTML ) {
 23           // 儘量快地找到目標節點, 選擇器類型是id,標籤和類
 24           // rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/
 25           // 將selector按 #[id] / [tag] / .[class]的順序捕獲到數組中,數組的第一個元素是原始值
 26           // 捕獲結果中'#'和'.'會被移除
 27           if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) {
 28                // 加速: Sizzle("#ID")
 29                if ( (m = match[1]) ) {
 30                     if ( nodeType === 9 ) {
 31                          elem = context.getElementById( m );
 32                          // 檢查Blackberry 4.6返回的已經不在document中的parentNode
 33                          if ( elem && elem.parentNode ) {
 34                               // IE, Opera, Webkit有時候會返回name == m的元素
 35                               if ( elem.id === m ) {
 36                                    results.push( elem );
 37                                    return results;
 38                               }
 39                          } else {
 40                               return results;
 41                          }
 42                     } else {
 43                          // 上下文不是document
 44                          if ( context.ownerDocument && 
 45 
 46                               (elem = context.ownerDocument.getElementById( m )) &&
 47                               contains( context, elem ) && elem.id === m ) {
 48 
 49                               results.push( elem );
 50                               return results;
 51                          }
 52                     }
 53                 // 加速: Sizzle("TAG")
 54                // 因爲返回是一個數組,所以須要讓這個數組做爲參數數組並利用push.apply調用將其拼接到results後面
 55                } else if ( match[2] ) {
 56                     push.apply( results, context.getElementsByTagName( selector ) );
 57                     return results;
 58                // 加速: Sizzle(".CLASS")
 59                // push.apply的使用緣由同上
 60                } else if ( (m = match[3]) && support.getElementsByClassName ) {
 61                     push.apply( results, context.getElementsByClassName( m ) );
 62                     return results;
 63                }
 64           }
 65 
 66           // 使用QSA, QSA: querySelectorAll, 原生的QSA運行速度很是快,所以儘量使用QSA來對CSS選擇器進行查詢
 67           // querySelectorAll是原生的選擇器,但不支持老的瀏覽器版本, 主要是IE8及之前的瀏覽器
 68           // rbuggyQSA 保存了用於解決一些瀏覽器兼容問題的bug修補的正則表達式
 69           // QSA在不一樣瀏覽器上運行的效果有差別,表現得很是奇怪,所以對某些selector不能用QSA
 70           // 爲了適應不一樣的瀏覽器,就須要首先進行瀏覽器兼容性測試,而後肯定測試正則表達式,用rbuggyQSA來肯定selector是否能用QSA
 71 
 72           if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
 73                nid = old = expando;
 74                newContext = context;
 75                newSelector = nodeType !== 1 && selector;
 76 
 77                // QSA 在以某個根節點ID爲基礎的查找中(.rootClass span)表現很奇怪,
 78                // 它會忽略某些selector選項,返回不合適的結果
 79                // 一個比較一般的解決方法是爲根節點設置一個額外的id,並以此開始查詢
 80                // IE 8 doesn't work on object elements 
 81                if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
 82                     groups = tokenize( selector );                  // 分析選擇器的詞法並返回一個詞法標記數組
 83                     if ( (old = context.getAttribute("id")) ) {     // 保存並設置新id
 84                          nid = old.replace( rescape, "\\$&" );
 85                     } else {
 86                          context.setAttribute( "id", nid );
 87                     }
 88                     nid = "[id='" + nid + "'] ";
 89                      i = groups.length;
 90                     while ( i-- ) {
 91                          groups[i] = nid + toSelector( groups[i] );     // 把新的id添加到選擇器標記裏
 92                     }
 93                     newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context;
 94                     newSelector = groups.join(",");                     // 構造新的選擇器
 95                }
 96                 if ( newSelector ) {                                    // 使用新的選擇器經過QSA來查詢元素
 97                     try {
 98                          push.apply( results,                          // 將查詢結果合併到results上
 99                               newContext.querySelectorAll( newSelector )
100                          );
101                          return results;
102                     } catch(qsaError) {
103                     } finally {
104                          if ( !old ) {
105                               context.removeAttribute("id");          // 若是沒有舊id,則移除
106                          }
107                     }
108                }
109           }
110      }
111       // 其餘selector,這些selector沒法直接使用原生的document查詢方法
112      return select( selector.replace( rtrim, "$1" ), context, results, seed );
113 }

 

rbuggy:測試QSA的Bug
 
     使用assert(function(div){})函數進程瀏覽器bug測試
 1 /**
 2 * Support testing using an element
 3 * @param {Function} fn Passed the created div and expects a boolean result
 4 */
 5 function assert( fn ) {
 6      var div = document.createElement("div");          // 建立測試用節點
 7      try {
 8           return !!fn( div );                          // 轉換fn的返回值爲boolean值
 9      } catch (e) {
10           return false;
11      } finally {
12           if ( div.parentNode ) {                      // 結束時移除這個節點
13                div.parentNode.removeChild( div );
14           }
15           div = null;                                  // IE瀏覽器中必須這樣,釋放內存
16      }
17 }

 

  • assert函數創建一個div節點,將這個div節點傳遞給回調函數;
  • div節點在assert函數結束時會被刪除,此時注意要刪除由回調函數建立的子節點,並將div賦值null以讓GC回收。
  • 回調函數利用新建的div節點做爲根節點,在這個根節點上建立一些測試用的節點進行測試;
一個bug測試例子:
 1 assert(function( div ) {
 2 
 3      // 建立一些子節點
 4      docElem.appendChild( div ).innerHTML = "<a id='" + expando + "'></a>" +
 5           "<select id='" + expando + "-\f]' msallowcapture=''>" +
 6           "<option selected=''></option></select>";
 7      ... // 其餘測試
 8      // 測試document.querySelectorAll()的正確性
 9      if ( div.querySelectorAll("[msallowcapture^='']").length ) {
10           rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
11 
12           // 肯定用於測試selector可否使用QSA的正則表達式
13 
14      }
15      ... // 其餘測試 
16 });

 

select方法:
 
     當沒法直接使用document的原生選擇器時,就會調用Sizzle.select.
     註釋裏寫到"A low-level selection function that works with Sizzle's compiled",這是一個低級選擇器,與Sizzle.compiled協做執行.
 
 1 // @param selector 已去掉頭尾空白的選擇器字符串 
 2 // @param context 執行匹配的最初的上下文(即DOM元素集合)。若context沒有賦值,則取document。 
 3 // @param results 已匹配出的部分最終結果。若results沒有賦值,則賦予空數組。 
 4 // @param seed 初始集合 
 5 
 6 select = Sizzle.select = function( selector, context, results, seed ) {
 7      var i, tokens, token, type, find,
 8           compiled = typeof selector === "function" && selector,
 9           match = !seed && tokenize( (selector = compiled.selector || selector) );
10       results = results || [];
11       // 當沒有seed或group時,儘量地減小操做
12      if ( match.length === 1 ) {
13 
14           // 若是根選擇器是id,利用快捷方式並設置context
15           tokens = match[0] = match[0].slice( 0 );
16           if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
17                     support.getById && context.nodeType === 9 && documentIsHTML &&
18                     Expr.relative[ tokens[1].type ] ) {
19 
20              // 使用Expr.find["ID"]查找元素,其中調用了context.getElementById方法
21              // 爲了兼容不一樣的瀏覽器,setDocument方法會測試不一樣的瀏覽器環境並構造一個使用與當前運行環境的Expr.find["ID"]元素
22              // 將id選擇器的返回結果做爲新的上下文
23 
24                context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
25                if ( !context ) {
26                     return results;     // 若是找不到id根元素直接返回results
27                 // Precompiled matchers will still verify ancestry, so step up a level
28                } else if ( compiled ) {
29                     context = context.parentNode;
30                }
31                // 移除第一個id選擇器
32                selector = selector.slice( tokens.shift().value.length );
33           }
34           // Fetch a seed set for right-to-left matching
35           // matchExpr["needsContext"]測試選擇器是否含有位置僞類,如:first,:even,或包含"> + ~"等關係
36 
37           // 若是包含將i賦值0,不然賦值tokens.length
38           i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
39 
40           // 遍歷tokens, 逐個查詢
41           while ( i-- ) {
42                token = tokens[i];
43                 // 遇到關係符"~ + > ."的時候跳出
44 
45                if ( Expr.relative[ (type = token.type) ] ) {
46                     break;
47                }
48 
49                // 根據type獲取查詢方法
50                if ( (find = Expr.find[ type ]) ) {
51                     // Search, expanding context for leading sibling combinators
52                     // rsibling = /[+~]/, 用於判斷同胞關係符
53                     if ( (seed = find(
54                          token.matches[0].replace( runescape, funescape ),
55                          rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context
56                     )) ) {
57 
58                          // 若是seed是空的或者沒有任何token了,就能夠提早返回
59                          // 不然,就根據新的seed和token,迭代地繼續搜索下去
60                          tokens.splice( i, 1 );
61                          selector = seed.length && toSelector( tokens );
62                          if ( !selector ) {
63                               push.apply( results, seed );
64                               return results;
65                          }
66                          break;
67                     }
68                }
69           }
70      }
71       // Compile and execute a filtering function if one is not provided
72      // Provide `match` to avoid retokenization if we modified the selector above
73      // 執行compile返回一個匹配器函數, 再利用這個返回的函數進行匹配;
74      ( compiled || compile( selector, match ) )(
75           seed,
76           context,
77           !documentIsHTML,
78           results,
79           rsibling.test( selector ) && testContext( context.parentNode ) || context
80      );
81      return results;
82 };

 

Compile方法:
 
 1 compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
 2      var i,
 3           setMatchers = [],
 4           elementMatchers = [],
 5           cached = compilerCache[ selector + " " ];     // 根據selector獲取cache中的匹配器
 6      // 若是還沒有建立這個匹配器,則須要建立一個
 7      if ( !cached ) {
 8           // 產生一個函數,這個函數包含一系列遞歸函數用來檢索每個元素
 9           if ( !match ) {
10                match = tokenize( selector );          // 解析選擇器詞法
11           }
12           i = match.length;
13           while ( i-- ) {
14                cached = matcherFromTokens( match[i] );     // 根據token建立匹配器
15                if ( cached[ expando ] ) {
16                     setMatchers.push( cached );
17                } else {
18                     elementMatchers.push( cached );
19                }
20           }
21           // Cache the compiled function
22           // matcherFromGroupMatchers 返回一個superMatcher
23           // compelerCache = createCache(), 根據selector創建匹配器方法cache
24           cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
25            //在cached中保存選擇器
26           cached.selector = selector;
27      }
28 
29      // 返回這個對應的匹配器
30      return cached;
31 };

 

 
總結
  • 調用jQuery.find的時候實際上就調用了Sizzle。Sizzle的實現的一個最基本思路是以最快的速度完成選擇器匹配,那如何纔可以完成呢?對於簡單不含其餘關係符的選擇器如(#id,tag,.class)就儘量得直接調用document.getElementById/ .getElementByTagName/ .getElementByClassName 等原生方法。這些DOM的基本方法的運行速度是最快的。而對於詞法更爲複雜的選擇器(包含關係符,僞類選擇器),首選是調用document.querySelectorAll。QSA的速度很是快,但在不一樣的瀏覽器上其運行效果不同,不少時候會有莫名奇妙的返回結果,所以使用前須要對瀏覽器進行測試,確保在肯定可以使用QSA的狀況下才進行這樣的調用,不然就須要調用Sizzle.select進行更低層次的元素匹配;
  • select的實現相對而言比較複雜,它首先須要對選擇器進行詞法分析,而後根據所獲得的詞法標記利用sizzle.compile構造出一系列匹配器,並將這些匹配器組合成一個更大的匹配方法,最後才執行這個匹配方法;關於select的更詳細具體的分析,留在往後再看;
  • 有select能夠看出來,鑑於瀏覽器的兼容性問題,尤爲是針對IE < 8的兼容性,jQuery在這方面做出了許多努力,使得代碼的編寫變得臃腫,執行效率也有所降低;
  • jQuery中使用的assert方法十分精妙,新建DOM節點元素做爲測試根節點,而後進行節點匹配測試。要注意的是,測試完成後刪除子節點,並對根節點賦值null,不然容易致使內存泄漏(IE);
  • jQuery的選擇器是使用頻率最高的方法之一,所以一切效率之上。從jQuery的源碼可學習到,如何在保證運行效率的前提下保證一個方法對瀏覽器的兼容性。
相關文章
相關標籤/搜索