jquery 源码分析七 - Sizzle
这篇主要是分析Sizzle引擎中最关键的一个函数Sizzle,这个函数接受四个参数selector,context,results,seed,这四个参数的意义分别是选择器,上下文环境,保存结果数组,还有一个未知。。
由于这个函数里面涉及了许多相关函数,所以先对主函数进行分析,并只是列出相关函数的作用。相关函数的分析会放在文章末尾。接下来先看主函数:
1 function Sizzle( selector, context, results, seed ) { 2 var match, elem, m, nodeType, 3 i, groups, old, nid, newContext, newSelector; 4 //各种常量定义 5 //设置合理的root节点,在网页中为document 6 if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { 7 setDocument( context ); 8 } 9 //确定上下文环境和结果数组 10 context = context || document; 11 results = results || []; 12 //如果无selector或者selector不为字符串,则直接返回results 13 if ( !selector || typeof selector !== "string" ) { 14 return results; 15 } 16 //如果上下文环境的节点不是element或者document的话,就直接返回 空数组 17 if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { 18 return []; 19 } 20 21 //documentIsHTML 用于确定工作环境是否是html 22 if ( documentIsHTML && !seed ) { 23 24 // rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/ 25 // 用于快速的检测是否是单一的ID,CLASS或TAG选择符 26 if ( (match = rquickExpr.exec( selector )) ) { 27 // ID选择 28 if ( (m = match[1]) ) { 29 if ( nodeType === 9 ) { 30 elem = context.getElementById( m ); 31 // 检测是否有parentNode存在,有些浏览器会返回已经被删除了的节点 32 if ( elem && elem.parentNode ) { 33 // 处理在IE, Opera, 和Webkit 会返回name等于 m 的元素 34 if ( elem.id === m ) { 35 results.push( elem ); 36 return results; 37 } 38 } else { 39 return results; 40 } 41 } else { 42 // 如果context不是document的话,要检测context对elem的包含情况。 43 if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && 44 contains( context, elem ) && elem.id === m ) { 45 results.push( elem ); 46 return results; 47 } 48 } 49 50 // TAG选择 51 } else if ( match[2] ) { 52 push.apply( results, context.getElementsByTagName( selector ) ); 53 return results; 54 55 // CLASS选择 56 } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) { 57 push.apply( results, context.getElementsByClassName( m ) ); 58 return results; 59 } 60 } 61 62 // querySelectorAll,处理复杂的选择 63 // rbuggyQSA用于检测querySelectAll的怪异特性 64 if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { 65 //expando是sizzle中唯一特征码 66 nid = old = expando; 67 newContext = context; 68 newSelector = nodeType === 9 && selector; 69 70 71 // querySelectorAll在element为根元素时行为很奇怪 72 // 我们可以给根元素一个特定的ID值 73 // 这样在selector最前面加上ID,就可以做到很好的筛选 74 // IE8中不支持在对象元素上用 75 if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { 76 //tokenize的作用是将selector中的筛选符分割开来并放到相应数组中 77 //例:tokenize('div#a') 78 // --->[[ 79 // {matches:{0:div,index:0,input:'div#a'}, 80 // type:'TAG', 81 // value:'div'}, 82 // {matches:{0:a,index:0,input:'div#a'}, 83 // type:'ID', 84 // value:'#a'} 85 // ]] 86 groups = tokenize( selector ); 87 88 //如果context存在id,那么就是用原来的id,否则就用expando 89 if ( (old = context.getAttribute("id")) ) { 90 nid = old.replace( rescape, "\\$&" ); 91 } else { 92 context.setAttribute( "id", nid ); 93 } 94 nid = "[id='" + nid + "'] "; 95 96 //将所有的选择符拼接 97 i = groups.length; 98 while ( i-- ) { 99 groups[i] = nid + toSelector( groups[i] ); 100 } 101 //探测是否有a+div或者[class~='abc']类似的情况,rsibling = /[+~]/ 102 newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context; 103 newSelector = groups.join(","); 104 } 105 //newSelector存在的话 106 if ( newSelector ) { 107 try { 108 //直接在context下用querySelectorAll,并将结果压入数组 109 push.apply( results, 110 newContext.querySelectorAll( newSelector ) 111 ); 112 return results; 113 } catch(qsaError) { 114 } finally { 115 //最后删除添加的id 116 if ( !old ) { 117 context.removeAttribute("id"); 118 } 119 } 120 } 121 } 122 } 123 124 // 其它情况都用select,这个函数以后分析 125 return select( selector.replace( rtrim, "$1" ), context, results, seed ); 126 }
接下来就是对里面那些相关函数的分析,这里面的涉及到的函数或变量主要有:setDocument,documentIsHTML,contains,toSelector,testContext,rbuggyQSA,tokenize
rbuggyQSA,tokenize,setDocument因为太绕(我连开5,6个jquery一起读了,然后再用chrome来追踪才读出来的,泪奔。。。),所以这次先不讲了。
首先是documentIsHTML,废话不多说,直接上代码:
1 documentIsHTML = !isXML( doc );
1 isXML = Sizzle.isXML = function( elem ) { 2 // 首先是确定根元素是否有documentElement 3 // 然后是确定documentElement节点名字是不是HTML 4 var documentElement = elem && (elem.ownerDocument || elem).documentElement; 5 return documentElement ? documentElement.nodeName !== "HTML" : false; 6 };
然后是contains,contains是在setDocument中定义的,因为contains是Sizzle中的全局变量,所以就可以到处用了。
1 // rnative = /^[^{]+\{\s*\[native \w/用于检测是否有原生函数 2 // docElem = 根元素.documentElement 3 // hasCompare = rnative.test( docElem.compareDocumentPosition ); 4 contains = hasCompare || rnative.test( docElem.contains ) ? 5 //首先是有compareDocumentPosition或者contains的情况 6 function( a, b ) { 7 var adown = a.nodeType === 9 ? a.documentElement : a, 8 bup = b && b.parentNode; 9 //先判断是否a直接包含b.parentNode 10 //否则,在确定bup后,先尝试用contains 11 //再用compareDocumentPosition 12 //compareDocumentPosition返回16表示a包含bup 13 //故要用&16将其转化为0或非0数字 14 return a === bup || !!( bup && bup.nodeType === 1 && ( 15 adown.contains ? 16 adown.contains( bup ) : 17 a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 18 )); 19 } : 20 //b循环向上取parent,若与a相同,则返回true 21 function( a, b ) { 22 if ( b ) { 23 while ( (b = b.parentNode) ) { 24 if ( b === a ) { 25 return true; 26 } 27 } 28 } 29 //循环不到,就返回false 30 return false; 31 };
然后是toSelector:
1 //循环,将每个对象下的value值合并到selector 2 function toSelector( tokens ) { 3 var i = 0, 4 len = tokens.length, 5 selector = ""; 6 for ( ; i < len; i++ ) { 7 selector += tokens[i].value; 8 } 9 return selector; 10 }
对于selector有空格的情况,toknize函数解析的时候会将空格也独立解析成一项,所以在这边合并时空格仍旧会出现,不会被丢弃。
最后是testContext,检测context是否为空,并且context下是否有getElementsByTagName方法,代码如下:
1 function testContext( context ) { 2 return context && typeof context.getElementsByTagName !== strundefined && context; 3 }
呼,搞定~