jQuery凭借选择器风靡全球,各大框架类库都争先开发本身的选择,一时间内选择器变为框架的标配javascript
早期的JQuery选择器和咱们如今看到的远不同。最初它使用混杂的xpath语法的selector。
第二代转换为纯css的自定义伪类,(好比从xpath借鉴过来的位置伪类)的sizzle,但sizzle也一直在变,由于他的选择器一直存在问题,一直到JQuery1.9才搞定,并最终全面支持css3的结构伪类。css
2005 年,Ben Nolan的Behaviours.js 内置了闻名于世的getElementBySelector,是第一个集成事件处理,css风格的选择器引擎与onload处理的类库,此外,往后的霸主prototype.js页再2005年诞生。但它勉强称的上是,选择器$与getElementByClassName在1.2出现,事件处理在1.3,所以,Behaviour.js还风光一时。html
本章从头到尾实验制造一个选择器引擎。再次,咱们先看看前人的努力:java
1.浏览器内置寻找元素的方法node
请不要追问05年以前开发人员是怎么在这种缺东缺西的环境下干活的。那时浏览器大战正酣。程序员发明navugator.userAgent检测进行"自保"!网景战败,所以有关它的记录很少。但IE确实留下很多资料,好比取得元素,咱们直接能够根据id取得元素自身(如今全部浏览器都支持这个特性),不经过任何API ,自动映射全局变量,在不关注全局污染时,这是个很酷的特性。又如。取得全部元素,使用document.All,取得某一种元素的,只需作下分类,如p标签,document.all.tags("p")。css3
有资料可查的是 getElementById , getElementByTagName是ie5引入的。那是1999年的事情,伴随一个辉煌的产品,window98,捆绑在一块儿,所以,那时候ie都倾向于为IE作兼容。程序员
(感兴趣的话参见让ie4支持getElementById的代码,此外,还有getElementByTagsName的实现)正则表达式
但人们很快发现问并没有法选取题了,就是IE的getElementById是不区分表单元素的ID和name,若是一个表单元素只定义name并与咱们的目标元素同名,且咱们的目标元素在它的后面,那么就会选错元素,这个问题一直延续到ie7.算法
IE下的getElementsByTagesName也有问题。当参数为*号通配符时,它会混入注释节点,并没有法选取Object下的元素。chrome
(解决办法略去)
此外,w3c还提供了一个getElementByName的方法,这个IE也有问题,它只能选取表单元素。
在Prototype.js还未到来以前,全部可用的只有原生选择器。所以,simon willson高出getElementBySelector,让世人眼前一亮。
以后的过程就是N个版本的getElementBySlelector,不过大多数是在simon的基础上改进的,甚至还讨论将它标准化!
getElementBySlelector表明的是历史的前进。JQuery在此时优势偏向了,prototype.js则在Ajax热浪中扶摇直上。不过,JQuery仍是胜利了,sizzle的设计很特别,各类优化别出心裁。
Netscape借助firefox还魂,在html引入xml的xpath,其API为document.evaluate.加之不少的版本及语法复杂,所以没有普及开来。
微软为保住ie占有率,在ie8上加入querySelector与querySlectorAll,至关于getElementBySelector的升级版,它还支持史无前例的伪类,状态伪类。语言伪类和取反伪类。此时,chrome参战,激发浏览器标准的热情和升级,ie8加入的选择器你们都支持了,还支持的更加标准。此时,还出现了一种相似选择器的匹配器————matchSelector,它对咱们编写选择器引擎特别有帮助,因为是版本号竞赛时诞生的,谁也不能保证本身被w3c采纳,都带有私有前缀。如今css方面的Selector4正在起草中,querySeletorAll也只支持到selector3部分,但其间兼容性问题已经很杂乱了。
2.getElementsBySelector
让咱们先看一下最古老的选择器引擎。它规定了许多选择器发展的方向。在解读中能涉及到不少概念,但没关系,后面有更详细的解释。如今只是初步了解下大概蓝图。
/* document.getElementsBySelector(selector) version 0.4 simon willson march 25th 2003 -- work in phonix0.5 mozilla1.3 opera7 ie6 */ function getAllchildren(e){ //取得一个元素的子孙,并兼容ie5 return e.all ? e.all : e.getElementsByTgaName('*'); } document.getElementsBySelector = function(selector){ //若是不支持getElementsByTagName 则直接返回空数组 if (!document.getElementsByTgaName) { return new Array(); } //切割CSS选择符,分解一个个单元格(每一个单元可能表明一个或多个选择器,好比p.aaa则由标签选择器和类选择器组成) var tokens = selector.split(' '); var currentContext = new Array(document); //从左至右检测每一个单元,换言此引擎是自顶向下选择元素 //若是集合中间为空,当即中至此循环 for (var i = 0 ; i < tokens.length; i++) { //去掉两边的空白(并非全部的空白都没有用,两个选择器组之间的空白表明着后代迭代器,这要看做者们的各显神通) token = tokens[i].replace(/^\s+/,'').replace(/\s+$/,''); //若是包含ID选择器,这里略显粗糙,由于它可能在引号里边。此选择器支持到属性选择器,则表明着多是属性值的一部分。 if (token.indexOf('#') > -1) { //假设这个选择器是以tag#id或#id的形式,可能致使bug(但这些暂且不谈,沿着做者的思路看下去) var bits =token.split('#'); var tagName = bits[0]; var id = bits[1]; //先用id值取得元素,而后断定元素的tagName是否等于上面的tagName //此处有一个不严谨的地方,element可能为null,会引起异常 var element = document.getElementById(id); if(tagName && element.nodeName.toLowerCase() != tagName) { //没有直接返回空结合集 return new Array(); } //置换currentContext,跳至下一个选择器组 currentContext = new Array(element); continue; } //若是包含类选择器,这里也假设它以.class或tag.class的形式 if (token.indexOf('.') > -1){ var bits = token.split('.'); var tagName = bits[0]; var className = bits[1]; if (!tagName){ tagName = '*'; } //从多个父节点,取得它们的全部子孙 //这里的父节点即包含在currentContext的元素节点或文档对象 var found = new Array;//这里是过滤集合,经过检测它们的className决定去留 var foundCount = 0; for (var h = 0; h < currentContext.length; h++){ var elements; if(tagName == '*'){ elements = getAllchildren(currentContext[h]); } else { elements = currentContext[h].getElementsByTgaName(tagName); } for (var j = 0; j < elements.length; j++) { found[foundCount++] = elements[j]; } } currentContext = new Array; for (var k = 0; k < found.length; k++) { //found[k].className可能为空,所以不失为一种优化手段,但new regExp放在//外围更适合 if (found[k].className && found[k].className.match(new RegExp('\\b'+className+'\\b'))){ currentContext[currentContextIndex++] = found[k]; } } continue; } //若是是以tag[attr(~|^$*)=val]或[attr(~|^$*)=val]的组合形式 if (token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/)){ var tagName = RegExp.$1; var attrName = RegExp.$2; var attrOperator = RegExp.$3; var attrValue = RegExp.$4; if (!tagName){ tagName = '*'; } //这里的逻辑以上面的class部分类似,其实应该抽取成一个独立的函数 var found = new Array; var foundCount = 0; for (var h = 0; h < currentContext.length; h++){ var elements; if (tagName == '*') { elements = getAllchildren(currentContext[h]); } else { elements = currentContext[h].getElementsByTagName(tagName); } for (var j = 0; j < elements.length; j++) { found[foundCount++] = elements[j]; } } currentContext = new Array; var currentContextIndex = 0; var checkFunction; //根据第二个操做符生成检测函数,后面的章节有详细介绍 ,请继续关注哈 switch (attrOperator) { case '=' : // checkFunction = function(e){ return (e.getAttribute(attrName) == attrValue);}; break; case '~' : checkFunction = function(e){return (e.getAttribute(attrName).match(new RegExp('\\b' +attrValue+ '\\b')));}; break; case '|' : checkFunction = function(e){ return (e.getAttribute(attrName).match(new RegExp('^'+attrValue+'-?')));}; break; case '^' : checkFunction = function(e) {return (e.getAttribute(attrName).indexOf(attrValue) == 0);}; break; case '$': checkFunction = function(e) { return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length);}; break; case '*': checkFunction = function(e) {return (e.getAttribute(attrName).indexOf(attrValue) > -1 );} break; default : checkFunction = function(e) {return e.getAttribute(attrName);}; } currentContext = new Array; var currentContextIndex = 0 ; for (var k = 0; k < found.length; k++) { if (checkFunction(found[k])) { currentContext[currentContextIndex++] = found[k]; } } continue; } //若是没有 # . [ 这样的特殊字符,咱们就当是tagName var tagName = token; var found = new Array; var foundCount = 0; for (var h = 0; h < currentContext.length; h++) { var elements = currentContext[h].getElementsByTgaName(tagName); for (var j = 0; j < elements.length; j++) { found[foundCount++] = elements[j]; } } currentContext = found; } return currentContext; //返回最后的选集 }
显然当时受网速限制,页面不会很大,也不可能有很复杂的交互,所以javascript尚未到大规模使用的阶段,咱们看到当时的库页不怎么重视全局污染,也不支持并联选择器,要求每一个选择器组不能超过两个,不然报错。换言之,它们只对下面的形式CSS表达式有效:
#aa p.bbb [ccc=ddd]
Css表达符将以空白分隔成多个选择器组,每一个选择器不能超过两种选取类型,而且其中之一为标签选择器
要求比较严格,文档也没有说明,所以很糟糕。但对当时编程环境来讲,已是喜出望外了。做为早期的选择器,它也没有想之后那样对结果集进行去重,把元素逐个按照文档出现的顺序进行排序,咱们在第一节指出的bug,页没有进行规避,多是受当时javascript技术交流太少。这些都是咱们要改进的地方。
3.选择器引擎涉及的知识点
本小节咱们学习上小节的大力的概念,其中,有关选择器引擎实现的概念大多数是从sizzle中抽取出来的,儿CSS表达符部分则是W3C提供的,首先从CSS表达符部分介绍。
h1 {color: red;font-size: 14px;}
其中,h1 为选择符,color和font-size为属性,red和14px为值,两组color: red和font-size: 14px;为它们的声明。
上面的只是理想状况,重构成员交给咱们CSS文件,里边的选择符但是复杂多了。选择符混杂着大量的标记,能够分割为更细的单元。总的来讲,分为四大类,十七种。此外,还包含选择引擎没法操做的伪元素。
四大类:指并联选择器、 简单选择器 、 关系选择器 、 伪类
并联选择器:就是“,”,一种不是选择器的选择器,用于合并多个分组的结果
关系选择器 分四种: 亲子 后代 相邻,通配符
伪类分为六种: 动做伪类, 目标伪类, 语言伪类, 状态伪类, 结构伪类, 取得反伪类。
简单的选择器又称为基本选择器,这是在prototype.js以前的选择器都已经支持的选择器类型。不过在css上,ie7才开始支持部分属性选择器。其中,它们设计的很是整齐划一,咱们能够经过它的一个字符决定它们的类型。好比id选择器的第一个字符为#,类选择器为. ,属性选择器为[ ,通配符选择器为 * ;标签选择器为英文字母。你能够能够解释为何没有特殊符号。jQuery就是使用/isTag = !/\W/.test( part )进行断定的。
在实现上,咱们在这里有不少原生的API可使用,如getElementById. getElementsByTagName. getElementsByClassName. document.all 属性选择器能够用getAttribute 、 getAttributeNode attributes, hasAttribute,2003年曾经讨论引入getElementByAttribute,但没成功,实际上,firefix上的XUI的同名就是当时的产物。不过属性选择器的确比较复杂,历史上他是分为两步实现的。
css2.1中,属性选择器又如下四种状态。
[att]:选取设置了att属性的元素,无论设定值是什么。
[att=val]:选取了全部att属性的值彻底等于val的元素。
[att~=val]:表示一个元素拥有属性att,而且该属性还有空格分割的一组值,其中之一为'val'。这个你们应该能联想到类名,若是浏览器不支持getElementsByClassName,在过滤阶段,咱们能够将.aaa转换为[class~=aaa]来处理
[att|=val]:选取一个元素拥有属性att,而且该属性含'val'或以'val-'开头
Css3中,属性选择器又增长三种形态:
[att^=val]:选取全部att属性的值以val开头的元素
[att$=val]:选取全部att属性的值以val结尾的元素
[att*=val]:选取全部att属性的值包含val字样的元素。
以上三者,咱们均可以经过indexOf轻松实现。
此外,大多选取器引擎,还实现了一种[att!=val]的自定义属性选择器。意思很简单,选取全部att属性不等于val的元素,着正好与[att=val]相反。这个咱们也能够经过css3的去反伪类实现。
咱们再看看关系选择器。关系选择器是不能单独存在的,它必须在其余两类选择器组合使用,在CSS里,它必须夹在它们中间,但选择器引擎可能容许放在开始。在很长时间内,只存在后代选择器(E F),就在两个选择器E与F之间的空白。css2.1又增长了两个,亲子选择器(E > F)与相邻选取(E + F),它们也夹在两个简单选择器之间,但容许大于号或加号两边存在空白,这时,空白就不是表示后代选择器。CSS3又增长了一个,兄长选择器(E ~ F),规则同上。CSS4又增长了一个父亲选取器,不过其规则一直在变化。
后代选择器:一般咱们在引擎内构建一个getAll的函数,要求传入一个文档对象或元素节点取得其子孙。这里要特别注意IE下的document.all,getElementByTagName 的("*")混入注释节点的问题。
亲子选择器:这个咱们若是不打算兼容XML,直接使用children就行。不过在IE5-8它都会混入注释节点。下面是兼容列状况。
chrome :1+ firefox:3.5+ ie:5+ opera: 10+ safari: 4+
function getChildren(el) { if (el.childElementCount) { return [].slice.call(el.children); } var ret = []; for (var node = el.firstChild; node; node = node.nextSibling) { node.nodeType == 1 && ret.push(node); } return ret; }
相邻选择器: 就是取得当前元素向右的一个元素节点,视状况使用nextSibling或nextElementSibling.
function getNext (el) { if ("nextElementSibling" in el) { return el.nextElementSibling } while (el = el.nextSibling) { if (el.nodeType === 1) { return el; } } return null }
兄长选择器:就是取其右边的全部同级元素节点。
function getPrev(el) { if ("previousElementSibling" in el) { return el.previousElementSibling; } while (el = el.previousSibling) { if (el.nodeType === 1) { return el; } } return null; }
上面提到的childElementCount 、 nextElementSibling是08年12月经过Element Traversal规范的,用于遍历元素节点。加上后来补充的parentElement,咱们查找元素就很是方便。以下表
遍历全部子节点 | 遍历全部子元素 | |
第一个 | firstChild | firstElementChild |
最后一个 | lastChild | lastElementChild |
前面的 | previousSibling | previousElementSibling |
后面的 | nextSibling | nextElementSibling |
父节点 | parentNode | parentElement |
数量 | length | childElementCount |
伪类
伪类是选择器家族中最庞大的家族,从css1开始,以字符串开头,到css3时代,出现了要求传参的机构伪类和去反伪类。
(1).动做伪类
动做伪类又分为连接伪类和用户行为伪类,其中,连接伪类由:visted和:link组成,用户行为伪类分为:hover,:active, :focus组成。这这里咱们基本上只能模拟:link,而在浏览器的原生的querySeletorAll对它们的支持也存在差别,ie8-ie10存在取:link错误,它只能取a的标签,实际:link指代a aera link这三种标签,这个其它标签浏览器都正确。另外,opera,safari外,其它浏览器取:focus都正常,除opera外,其它浏览器取得:hover都正确。剩下:active和:visted都正确。剩下的:active与visted都为零。
window.onload = function(){ document.querySelector("#aaa").onclick = function() { alert(document.querySelectorAll(":focus").length) ;// =>1 } document.querySelector("#bbb").onclick = function() { alert(document.querySelectorAll(":hover").length); //=> 4 //4 ,html body p a } function test() { alert(document.querySelectorAll(":link").length);//=> 3 } }
伪类没有专门的api获得结果集合,所以,咱们须要经过上一次获得的结果集就行过滤。在浏览器中,咱们能够经过document.links获得部分结果,所以不包含link标签。所以,最好的方法是断定它的tagName是否等于A,LINK,AREA中的其中一个。
(2).目标伪类
目标伪类即:target伪类,指其id或者name属性与url的hash部分(#以后的部分),匹配上的元素。
假如一个文档,其id为section_2,而url中的hash部分也是#section_2,那么它就是咱们要取的元素。
Sizzle中过滤的函数以下:
"target": function(elem) { var hash = window.location && window.location.hash; return hash && hash.slice(1) === elem.id; }
(3).语言伪类
语言伪类即:length伪类,用来设置使用特殊语言的内容样式,如:lang(de)的内部应该为德语,须要特殊处理。
注意:lang 虽然为DOM元素的一个属性,但:lang伪类与属性选择器有所不一样,具体表现:lang伪类具备“继承性”,以下面的html表示的文档
<html> <head> </head> <body lang="de"> <p>一个段落</p> </body> </html>
若是使用[lang=de]则只能选择到body元素,由于p元素没有lang属性,可是使用:lang(de)则能够同时选择到body和p元素,表现出继承性。
"lang": markFunction(function(lang) { //lang value must be a valid iddentifider if (!ridentifier,test(lang || "") + lang); } lang = lang.replace(runescape, funescape).toLowerCase(); return function(elem) { var ememLang; do { if ((ememLang = documentIsXML ? elem.getAttribute("xml:lang") || elem.getAttribute("lang"):elem.lang)){ elemLang = elemLang.toLowerCase(); return elemLang === lang || elemLang.indexOf(lang + "-") === 0; } } while ((elem = elem.parentNode) && elem.nodeType === 1); return false; } }),
(4).状态伪类
状态伪类用于标记一个UI元素的当前状态,有:checked , :enabled , :disable 和 :indeterminate这四个伪类组成。咱们能够分别经过元素的checked , disabled , indeteminate属性进行断定。
(5).结构伪类
它又能够分为三种,根伪类,子元素过滤伪类,空伪类。
根伪类是由它在文档的位置断定,子元素过滤伪类是根据它在其父亲的全部孩子的位置或标签类型断定。空伪类是根据它孩子的个数断定。
:root伪类 用于选取根元素,在html文档中,一般是html元素。
:nth-child 是全部子元素的过滤伪蓝本,其它8种都是由它衍生出来的。它带有参数,能够是纯数字,代数式或单词,若是是数字,则从1计起,若是是代数式,n则从0递增,很是很差理解的规则。
:nth-child(2)选取当前父节点的第2个子元素
:nth-child(n+4) 选取大于等于4的的标签,咱们能够将n当作自增量(0 <= n <= parent.children.length),此代数的值因变量。
:nth-child(-n+4)选取小于等于4标签
:nth-child(2n)选取偶数标签,2n也能够是even
:nth-child(2n-1)选取奇数标签,2n-1也能够是odd
:nth-child(3n+1)表示没三个为一组,选取它的第一个
:nth-last-child与:nth-child差很少,不过是从后面取起来。好比:nth-last-child(2)
:nth-of-type和nth-last-of-type与:nth-child和nth-last-child相似,规则是将当前元素的父节点的全部元素按照tagName分组,只要其参数符合它在那一组的位置就被匹配到。好比:nth-of-type(2),另一例子,nth-of-type(even).
:frist-child用于选取第一个子元素,效果等同于:nth-child(1)
:last-child用于选取最后一个元素,效果等同于:nth-last-child(1)
:only-child用于选择惟一的子元素,当子元素的个数超过1时,选择器失效。
:only-of-type将父节点的元素按照tagName分组,若是某一组只有一个元素,那么就选择这些元素返回。
:empty 用于选择那些不包含任何元素的节点,文本节点,CDATA节点的元素,但容许里边只存在注释节点。
Sizzle中:empty的实现以下:
"empty": function(elem) { for (elem = elem.firstChild; elem ; elem = elem.nextSibling) { if (elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4){ return false; } } return true; },
mootools中的Slick.实现以下。
"empty": function(node) { var child = node.firstChild; return !(child && child.nodeType == 1) && !(node.innerText || node.textContent || '').length; },
(6).去反伪类
去反伪类即:not伪类,其参数为一个或多简单选择器,里边用逗号隔开。在jQuery等选择器引擎中容许你传入其它类型的选择器,甚至能够进行多个去反伪类嵌套。
(7).引擎实现时涉及的概念
种子集:或者叫候选集,若是css选择符很是复杂,咱们要分几步才能获得咱们想要的元素。那么第一次获得的元素集合就叫种子集。在Sizzle中,这样基本从右到左,它的种子集中就有一部分为咱们最后获得的元素。若是选择器引擎是从左至右的选择,那么它们只是咱们继续查找它的子元素或兄弟的“据点”而已。
结果集:选择器引擎最终返回的元素集合们如今约定俗成,它要保持与querySlectorAll获得的结果一致,即,没有重复元素。元素要按照他们在DOM树上出现的顺序排序。
过滤集:咱们选取一组元素后,它以后每个步骤要处理的元素集合均可以称为过滤集。好比p.aaa,若是浏览器不支持querySelectorAll,若支持getElementByClassName,那么咱们就用它获得种子集,而后经过className进行过滤。显然在大多数状况下,前者比后者快多了。同理,若是存在ID选择器,因为ID在一个文档中不重复,所以,使用ID查找更快。在Sizzle下若是不支持QuerySlectorAll,它会只能地以ID,class,Tag顺序去查找。
选择器群组:一个选择符被并联选择器","划分红的每个大组
选择器组:一个选择器群组被关系选择器划分为的第一个小分组。考虑到性能,每个小分组都建议加上tagName,所以这样在IE6方便使用documentsByTagName,好比p div.aaa 比 p.aaa快多了。前者两次getElementsByTagName查找,最后用className过滤。后者是经过getElementsByTagName获得种子集,而后再取它们的全部子孙,明显这样获得的过滤集比前者数量多不少。
从实现上,你能够从左至右,也能够像sizzle那样从右至左(大致上是,实际状况复杂不少)。
另外,选择器也分为编译型和非编译型,编译型是Ext发明的,这个阵营的选择器中有Ext,Qwrap,NWMatchers,JindoJS.非编译型的就更多了,如Sizzle,Icarus,Slick,YUI,dojo,uupaa,peppy...
还有一些利用xpath实现的选择器,最著名的是base2,它先实现了xpath那一套,方便IE也是有document,evaluate,而后将css选择符翻译成xpath。其它比较著名的有casperjs,DOMAssistant.
像sizzle mootools Icarus等还支持选择XML元素(由于XML仍是一种比较重要的数据传输格式。后端经过XHR返回咱们的就多是XML),这样咱们经过选择器引擎抽取所须要的数据就简单多了。
4.选择器引擎涉及的通用函数
1. isXML
最强大的前几名选择器引擎都能操做XML文档,但XML与HTMl存在很大的差别,没有className,getElementById,而且nodeName须要区分大小写,在旧版IE中还不能直接给XML元素添加自定义属性。所以,这些区分很是有必要。所以咱们看一下各大引擎的实现吧。
Sizzle的实现以下。
var isXML = Sizzle.isXML = function (elem) { var documentElement = elem && (elem.ownDocument || elem).documentElement; return documentElement ? documentElement.nodeName !== "HTML" : false; };
但这样作不严谨,由于XML的根节点多是HTML标签,好比这样建立一个XML文档:
try{ var doc = document.implementation.createDocument(null, 'HTML', null); alert(doc.documentElement) alert(isXML(doc)) } catch (e) { alert("不支持creatDocument") }
咱们来看看mootools的slick的实现
isXML = function(document){ return (!!document.xmlVersion) || (!!document.xml) || (toString.call(document) == '[object XMLDocument]') || (document.nodeType == 9 && document.documentElement.nodeName != 'HTML'); };
mootools用到了大量的属性来进行断定,从mootools1.2到如今还没什么改动,说明仍是很稳定的。咱们再精简一下。
在标准浏览器里,暴露了一个建立HTML文档的构造器HTMLDocument,而IE下的XML元素又拥有selectNodes:
var isXML = window.HTMLDocument ? function(doc) { return !(doc instanceof HTMLDocument) } : function (doc) { return "selectNodes" in doc }
不过这些方法都是规范,javascript对象能够随意添加,属性法很容易被攻破,最好使用功能法。不管XML或HTML文档都支持createElement方法,咱们断定建立了元素的nodeName是否区分大小写。
var isXML = function(doc) { return doc.createDocument("p").nodeName !== doc.createDocument("p").nodeName; }
这是目前能给出最严谨的函数了。
2.contains
contains方法就是断定参数1是否包含参数2。这一般用于优化。好比早期的Sizzle,对于#aaa p.class选择符,它会优先用getElementByClassName或getElementsByTagName取种子集,而后就不继续往左走了,直接跑到最左的#aaa,取得#aaa元素,而后经过contains方法进行过滤。随着Sizzle体积进行增大,它如今只剩下另外一个关于ID的用法,即:若是上下文对象非文档对象,那么它会取得其ownerDocument,这样就能够用getElementById,而后用contains方法进行验证!
//sizzle 1.10.15 if (context.ownerDocument && (elem = context.ownerDocument.getElementById(m)) && contains(context, elem) && elem.id === m) { results.push(elem); return results; }
contains的实现。
var rnative = /^[^]+\{\s*\[native \w/, hasCompare = rnative.test( docElem.compareDocumentPosition ), contains = hasCompare || rnative.test(docElem.contains) ? function(a, b){ var adown = a.nodeType === 9 ? a.documentElement : a, bup = b && b.parentNode; return a === bup || !!(bup && bup.nodeType === 1 && ( adown.contains ? adown.contains(bup) : a.compareDocumentPosition && a.compareDocumentPosition(bup) & 16 )); } : function (a, b) { if (b) { while ((b = b.parentNode)) { if (b === a) { return true } } } return false; };
它本身作了预断定,但这时传入xml元素节点,可能就会出错,所以建议改为实时断定。虽然每次都进入都断定一次那个原生API。
mass framework的实现方式:
//第一个节点是否包含第二个节点,same容许二者相等 if (a === b){ return !!same; } if (!b.parentNode) return false; if (a.contains) { return a.contains(b); } else if (a.compareDocumentPosition) { return !!(a.compareDocumentPosition(b) & 16); } while ((b = b.parentNode)) if (a === b) return true; return false; }
如今来解释一下contans与compareDocumentPosition这两个API。contains原来是IE私有的,后来其余浏览器也借鉴这方法。如fireFox在9.0也安装了此方法。它是一个元素节点的方法,若是另外一个等于或包含它的内部,就返回true.compareDocumentPosition是DOM的level3 specification定义的方法,firefox等标准浏览器都支持,它等于断定两个节点的关系,而不可是包含关系。这里是NodeA.compareDocumentPosition(Node.B)包含你能够获得的信息。
Bits | ||
000000 | 0 | 元素一致 |
000001 | 1 | 节点在不一样的文档(或者一个在文档以外) |
000010 | 2 | 节点B在节点A以前 |
000100 | 4 | 节点A在节点B以前 |
001000 | 8 | 节点B包含节点A |
010000 | 16 | 节点A包含节点B |
100000 | 32 | 浏览器的私有使用 |
有时候,两个元素的位置关系可能连续知足上表的二者状况,好比A包含B,而且A在B的前面,那么compareDocumentPosition就返回20.
旧版本的IE不支持compareDocumentPosition。jQuery做者john resig写了个兼容函数,用到IE的另外一个私有实现,sourceIndex, sourceIndex会根据元素位置的从上到下,从左至右依次加1,好比HTML标签的sourceIndex为0,Head的标签为1,Body的标签为2,Head的第一个子元素为3.若是元素不在DOM树,那么返回-1.
function comparePosition(a, b) { return a.compareDocumentPosition ? a.compareDocumentPosition(b) : a.contains ? (a != b && a.contains(b) && 16) + (a.sourceIndex >= 0 && b.sourceIndex >= 0 ? (a.sourceIndex < b.sourceIndex && 4) + (a.sourceIndex > b.sourceIndex && 2): 1) : 0; }
3.节点的排序与去重
为了让选择器引擎搜到的结果集尽量接近原生的API结果(由于在最新的浏览器中,咱们可能只使用querySlelectorAll实现),咱们须要让元素节点按它们在DOM树出现的顺序排序。
IE早期的版本,咱们可使用sourceIndex进行排序。
标准的浏览器可使用compareDocumentPosition.在上小节中介绍了它能够断定两个节点的位置关系。咱们只要将它们的结果按位于4不等于0就知道其先后顺序了。
var compare =comparerange.compareBoundaryPoints(how, sourceRange);
compare:返回1,0,-1(0为相等,1为compareRange在sourceRange以后,-1为compareRange在sourceRange以前)
how:比较那些边界点:为常数
特别的状况发生于要兼容旧版本标准浏览器与XML文档时,这时只有一些很基础的DOM API,咱们须要使用nextSibling来断定谁是哥哥,谁是“弟弟”。若是他们不是同一个父节点,咱们就须要将问题转化为求最近公共祖先,断定谁是“父亲”,谁是"伯父"节点。
到这里,已是很单纯的算法问题了。实现的思路有不少,最直观最笨的作法是,不断向上获取他们的父节点,直到HTML元素,连同最初的那个节点,组成两个数组,而后每次取数组最后的元素进行比较,若是相同就去掉。一直取到不相同为止。最后用nextSibling结束。下面是测试的代码。须要本身去试验。
window.onload = function () { function shuffle(a) { //洗牌 var array = a.concat(); var i = array.length; while (i) { var j = Math.floor(Math.random() * i); var t = array[--i]; array[i] = array[j]; array[j] = t; } return array; } var log = function(s) { //查看调试消息 window.console && window.console.log(s) } var sliceNodes = function(arr){ //将NodeList转化为纯数组 var ret = [], i = arr.length; while (i) ret [--i] = arr[i]; return ret; } var sortNodes = function(a, b) { //节点排序 var p = "parentNode", ap = a[p], bp = b[p]; if (a === b) { return 0 } else if (ap === bp) { while (a = a.nextSibling) { if (a === b) { return -1 } } return 1 } else if (!ap) { return -1 } else if (!bp) { return 1 } var al = [], ap = a //不断往上取,一值取到HTML while (ap && ap.nodeType === 1) { al[al.length] = ap ap = ap[p] } var bl =[], bp = b; while (bp && bp.nodeType === 1) { bl[bl.length] = bp bp = bp[p] } //而后一块儿去掉公共祖先 ap = al.pop(); bp = bl.pop(); while(ap === bp) { ap = al.pop(); bp = bl.pop(); } if (ap && bp) { while (ap = ap.nextSibling) { if (ap === bp) { return -1 } } return 1; } return ap ? 1 : -1 } var els = document.getElementsByTagName("*") els = sliceNodes(els); //转换成纯数组 log(els); els = shuffle(els); //洗牌的过程(模拟选择器引擎最初获得的结果集的状况) log(els); els = els.sort(sortNodes); //进行节点排序 log(els) }
它没打算支持xml与旧版标准浏览器,不支持就不会排序。
mass Framework的icarus引擎,结合了一位编程高手JK的算法,在排序去重远胜Sizzle。
其特色在于,不管sizzle或者slick,它们都是经过传入比较函数进行排序。而数组的原生sort方法,当它传一个比较函数时,无论它内部用哪一种排序算法(ecma没有规定sort的具体实现方法,所以,各个浏览器而异,好比FF2使用堆排序,FF3使用归并排序,IE比较慢,具体的算法不明,可能为冒泡或插入排序,而chrome为了最大效率,采用了二者算法:
http://yiminghe.iteye.com/blog/469713(玉伯大神) 附加一个排序方法:http://runjs.cn/code/tam0czbv
),都须要屡次比对,因此很是耗时间,若是能设计让排序在不传参的状况下进行,那么速度就会提升N倍。
下面是具体的思路(固然只能用于IE或早期的Opeara,因此代码不贴出来。)
i.取出元素节点的sourceIndex值,转换为一个String对象
ii.将元素节点附在String对象上
iii.用String对象组成数组
iiii.用原生的sor进行string对象数组排序
iiiii.在排序好的String数组中,排序取出元素节点,便可获得排序好的结果集。
在这里贴两篇关于排序和节点选择的前辈文章:扩展阅读
http://www.cnblogs.com/jkisjk/archive/2011/01/28/array_quickly_sortby.html
http://www.cnblogs.com/jkisjk/archive/2011/01/28/1946936.html
扩展阅读 (参考司徒先生的多代选择器引擎:)http://www.cnblogs.com/rubylouvre/archive/2011/11/10/2243838.html
4.切割器
选择器下降了javascript的入行门槛,它们在选择元素时都很随意,一级一级地向上加ID类名,致使选择符很是长,所以,若是不支持querySlelectorAll,没有一个原生API能承担这份工做,所以,咱们经过使用正经常使用户对选择符进行切割,这个步奏有点像编译原理的词法分析,拆分出有用的符号法来。
这里就拿Icarus的切割器来举例,看它是怎么一步步优化,就知道这工做须要多少细致。
/[\w\u00a1-\uFFFF][\w\u00a1-\uFFFF-]*|[#.:\[][\w\(\)\]]+|\s*[>+~,*]\s*|\s+/g
好比,对于".td1,div a,body"上面的正则可完美将它分解为以下数组:
[".td1",",","div"," ","*",",","body"]
而后咱们就能够根据这个符号流进行工做。因为没有指定上下文对象,就从document开始,发现第一个是类选择器,能够用getElementsByClassName,若是没有原生的,咱们仿照一个也不是难事。而后是并联选择器,将上面获得的结果放进结果集。接着是标签选择器,使用getElementsByTgaName。接着是后代选择器,这里能够优化,咱们能够预先查看一个选择器群组是什么,发现是通配符选择器,所以继续使用getElementsByTgaName。接着又是并联选择器,将上面结果放入结果集。最后一个是标签选择器,又使用getElementsByTgaName。最后是去重排序。
显然,有切割好的符号,工做简单多了。
但没有东西一开始就是完美的,好比咱们遇到一个这样的选择符,"nth-child(2n+1)".这是一个单独的子元素过滤伪类,它不该该在这里被分析。后面有专门的正则对它的伪类名与传参进行处理。在切割器里,它能获得最小的词素是选择器!
因而切割器进行改进
//让小括号里边的东西不被切割 var reg = /[\w\u00a1-\uFFFF][\w\u00a1-\uFFFF-]*|[#.:\[](?:[\w\u00a1-\uFFFF-]|\([^\)]*\)|\])+|(?:\s*)[>+~,*](?:\s*)|\s+/g
咱们不断增长测试样例,咱们问他愈来愈多,如这个选择符 :“.td1[aa='>111']” ,在这种状况下,属性选择器被拆碎了!
[".td1","[aa",">","111"]
因而正则改进以下:
//确保属性选择器做为一个完整的词素 var reg = /[\w\u00a1-\uFFFF][\w\u00a1-\uFFFF-]*|[#.:](?:[\w\u00a1-\uFFFF-]|\S*\([^\)]*\))+|\[[^\]]*\]|(?:\s*)[>+~,*](?:\s*)|\s+/g
对于选择符"td + div span",若是最后有一大堆空白,会致使解析错误,咱们确保后代选择器夹在两个选择器之间
["td", "+", "div", " ", "span", " "]
最后一个选择器会被咱们的引擎认做是后代选择器,须要提早去掉
//缩小后迭代选择器的范围 var reg = /[\w\u00a1-\uFFFF][\w\u00a1-\uFFFF-]*|[#.:](?:[\w\u00a1-\uFFFF-]|\S+\([^\)]*\))+|\[[^\]]*\]|(?:\s*)[>+~,*](?:\s)|\s(?=[\w\u00a1-\uFFFF*#.[:])/g
若是咱们也想将前面的空白去掉,可能不是一个单独的正则能作到的。如今切割器已经被咱们搞的至关复杂了。维护性不好。在mootools中等引擎中,里边的正则表达式更加复杂,多是用工具生成的 。到了这个地方,咱们须要转换思路,将切割器该为一个函数处理。固然,它里边也少了很多正则。正则是处理字符串的利器。
var reg_split = /^[\w\u00a1-\uFFFF\-\*]+|[#.:][\w\u00a1-\uFFFF-]+(?:\([^\])*\))?|\[[^\]]*\])|(?:\s*)[>+~,](?:\s*)|\s(?=[\w\u00a1-\uFFFF*#.[:])|^\s+/; var slim = /\s+|\s*[>+~,*]\s*$/ function spliter(expr) { var flag_break = false; var full = []; //这里放置切割单个选择器群组获得的词素,以,为界 var parts = []; //这里放置切割单个选择器组获得的词素,以关系选择器为界 do { expr = expr.replace(reg_split,function(part) { if (part === ",") { //这个切割器只处理到一个并联选择器 flag_break = true; } else { if (part.match(slim)) { //对于关系并联。通配符选择器两边的空白进行处理 //对parts进行反转,例如 div.aaa,反转先处理aaa full = full.concat(parts.reverse(),part.replace(/\s/g, '')); parts = []; } else { parts[parts.length] = part } } return "";//去掉已经处理了的部分 }); if (flag_break) break; } while (expr) full =full.concat(parts.reverse()); !full[0] && full.shift(); //去掉开头的第一个空白 return full; } var expr = " div > div#aaa,span" console.log(spliter(expr)); //=> ["div", ">", "div"]
固然,这个相对于sizzle1.8与slick等引擎来讲,不值一提,须要有很是深厚的正则表达式功力,深层的知识宝库自动机理论才能写出来。
5.属性选择器对于空白字符的匹配策略
上文已经介绍过属性选择器的七种形态了,但属性选择器并无这么简单,在w3c草案对属性选择器[att~=val]提到了一个点,val不能为空白字符,不然比较值flag(flag为val与元素实际值比较结果)返回false。若是querySlelectorAll测试一下属性其余状态,咱们会获得更多相似结果。
<!DOCTYPE html> <html> <head> <title></title> <meta charset="utf-8"> </head> <body> <script type="text/javascript"> window.onload = function () { console.log(document.querySelector("#test1[title='']")); //<div title="" id="test1"></div> console.log(document.querySelector("#test1[title~='']")); // null console.log(document.querySelector("#test1[title|='']")); //<div title="" id="test1"></div> console.log(document.querySelector("#test1[title^='']")); //null console.log(document.querySelector("#test1[title$='']")); // null console.log(document.querySelector("#test1[title*='']")); //null console.log("==========================================") console.log(document.querySelector("#test2[title='']")); //null console.log(document.querySelector("#test2[title~='']")); //null console.log(document.querySelector("#test2[title|='']")); //null console.log(document.querySelector("#test2[title^='']")); //null console.log(document.querySelector("#test2[title$='']")); //null console.log(document.querySelector("#test2[title*='']")); //null } </script> <div title="" id="test1"></div> <div title="aaa" id="test2"></div> </body> </html>
换言之,只要val为空,除=或|=除外,flag必为false,而且非=,!=操做符,若是取得值为空白字符,flag也必为false.
6.子元素过滤伪类的分级与匹配
子元素过滤伪类是css3新增的一种选择器。比较复杂,这里单独放出来讲。首先,咱们要将它从选择符中分离出来。这个通常由切割器搞定。而后咱们用正则将伪类名与它小括号里的传参分解出来。
以下是Icarus的作法
var expr = ":nth-child(2n+1)" var rsequence = /^([#\.:])|\[\s*]((?:[-\w]|[^\x00-\xa0]|\\.)+)/ var rpseudo = /^\(\s*("([^"]*)"|'([^']*)'|[^\(\)]*(\([^\(\)]*\))?)\s*\)/ var rBackslash = /\\/g //这里把伪类从选择符里分散出来 match = expr.match(rsequence); //[":nth-child",":",":nth-child"] expr = RegExp.rightContext; //用它左边的部分重写expr--> "(2n+1)" key = (match[2] || "").replace(rBackslash, ""); //去掉换行符 key=--> "nth-child" switch (match[1]) { case "#": //id选择器 略 break; case ".": //类选择器 略 break; case ":": //伪类 略 tmp = Icarus.pseudoHooks[key]; //Icarus.pseudoHooks里边放置咱们所能处理的伪类 expr = RegExp.rightContext;//继续取它左边的部分重写expr if ( !! ~key.indexOf("nth")) { //若是子元素过滤伪类 args = parseNth[match[1]] || parseNth(match[1]);//分解小括号的传参 } else { args = match[3] || match [2] || match[1] } break; default: //属性选择器 略 break; }
这里有个小技巧,咱们须要不断把处理过的部分从选择器中去掉。通常选择器引擎是使用expr = expr.replace(reg,"")进行处理,Icarus巧妙的使用正则的RegExp.rightContext进行复写,将小括号里边的字符串取得咱们经过parseNTH进行加工。将数字1,4,单词even,odd,-n+1等各类形态转换为an+b的形态。
function parseNth (expr) { var orig = expr expr = expr.replace(/^\+|\s*/g, '');//清除掉无用的空白 var match = (expr === "even" && "2n" || expr === "odd" && "2n+1" || !/\D/.test(expr) && "0n+" + expr || expr).match(/(-?)(\d*)n([-+]?\d*)/); return parse_nth[orig] = { a: (match[1] + (match[2] || 1) - 0 , b: match[3] - 0) }; }
parseNth是一个缓存函数,这样能避免重复解析,提升引擎的整体性能(缓存的精髓)
关于缓存的利用,能够参看Icarus Sizzle1.8+ Slice等引擎,需求本身可寻找。
5.sizzle引擎
jQuery最大的特色就是其选择器,jQuery1.3开始装其Sizzle引擎。sizzle引擎与当时主流的引擎大不同,人们说它是从右至左选择(虽然不对,但大体方向如此),速度远胜当时选择器(不过当时也没有什么选择器,所以sizzle一直自娱自乐)
Sizzle当时有如下几个特色
i容许关系选择器开头
ii容许反选择器套取反选择器
iii大量的自定义伪类,好比位置伪类(eq,:first:even....),内容伪类(:contains),包含伪类(:has),标签伪类(:radio,:input,:text,:file...),可见性伪类(:hidden,:visible)
iiii对结果进行去重,以元素在DOM树的位置进行排序,这样与将来出现的querySelector行为一致。
显然,搞出这么东西,不是一朝半夕的事情,说明john Resig研发了好久。当时sizzle的版本号为0.9.1,代码风格跟jQuery大不同,很是整齐清晰。这个风格一直延续到jQuery.1.7.2,Sizzle版本也跟上为1.7.2,在jQuery.1.8时,或sizzle1.8时,风格大变,首先里边的正则式是经过编译获得的,以求更加准确,结构也异常复杂,开始走ext那样编译函数的路子,经过多种缓存手段提升查询速度和匹配速度。
因为第二阶段的Sizzle蜕变尚未完成,每星期都在变,tokenize ,addMatcher,matcherFrom,Tokens,matcherFormGroupMachers,complile这些关键的内部函数都在改进,不断膨胀。咱们看是来看看sizzle1.72,这个版本是john Resing第一个阶段的最完美的思想结晶。
当时,Sizzle的总体结构以下:
i.Sizzle主函数,里边包含选择符的切割,内部循环调用住查找函数,主过滤函数,最后是去重过滤。
ii.其它辅助函数,如uniqueSort, matches, matchesSelector
iii.Sizzle.find主查找函数
iiii.Sizzle.filiter过滤函数
iiiii.Sizzle.selectors包含各类匹配用的正则,过滤用的正则,分解用的正则,预处理用的函数,过滤函数等
iiiiii.根据浏览器特征设计makeArray,sortOrder,contains等方法
iiiiiii.根据浏览器特征重写Sizzle.selectors中的部分查找函数,过滤函数,查找次序。
iiiiiiii.若浏览器支持querySelectorAll,那么用它重写Sizzle,将原来的sizzle做为后备方案包裹在新的sizzle里边
iiiiiiiii.其它辅助函数,如isXML,posProcess
下面使用一部分源码分析下1.7.2sizzle
var Sizzle = function(slelctor, context, results, seed) { //经过短路运算符,设置一些默认值 results = results || []; context = context || document; //备份,由于context会被改写,若是出现并联选择器,就没法确保当前节点是对于哪个context var origContext = context; //上下文对象必须是元素节点或文档对象 if (context.nodeType !== 1 && context.nodeType !== 9) { return []; } //选择符必须是字符,且不能为空 if (!slelctor || typeof slelctor !== "string") { return results; } var m, set, checkSet, extra, ret, cur, pop, i, prune = true, contextXML = Sizzle.isXML(context), parts = [], soFar = slelctor; //下面是切割器的实现,每次只处理到并联选择器,extra给留下次递归自身时做传参 //不过与其余引擎实现不一样的是,它没有一会儿切成选择器,并且切成选择器组与关系选择器的集合 //好比body div > div:not(.aaa),title //后代选择器虽然被忽略了,但在循环这个数组时,它默认每两个选择器组与关系选择器不存在就放在后代选择器到那个位置上 do { chunker.exec(""); //这一步主要讲chunker的lastIndex重置,固然是直接设置chunker.lastIndex效果也同样 m = chunker.exec(soFar); if (m) { soFar = m[3]; parts.push(m[1]); if (m[2]) { //若是存在并联选择器,就中断 extra = m[3]; break; } } } while (m); // 略.... }
接下来有许多分支,分别是对ID与位置伪类进行优化的(暂时跳过),着重几个重要概念,查找函数,种子集,映射集。这里集合sizzle源码。
查找函数就是Sizzle.selecters.find下的几种函数,常规状况下有ID,TAG,NAME三个,若是浏览器支持getElementByClassName,还会有Class函数。正如咱们前面所介绍的那样,getElementById,geyElementsByName,getElementsByTagName,geyElementByClassName不能彻底信任他们,即使是标准浏览器都会有bug,所以四大查找函数都作了一层封装,不支持返回undefined,其它则返回数组NodeList
种子集,或叫候选集,就是经过最右边的选择器组获得的元素集合,好比说"div.aaa span.bbb",最右边的选择器组就是"span.bbb" ,这时引擎就会根据浏览器支持的状况选择getElemntByTagName或者getElementClassName获得一组元素,而后经过className或tagName进行过滤。这时获得的集合就是种子集,Sizzle的变量名seed就体现了这一点
映射集,或叫影子集,Sizzle源码的变量名为checkSet。这是个怎样的东西呢?当咱们取得种子集后,而是将种子集赋值一份出来,这就是映射集。种子集是由选择器组选出来的,这时选择符不为空,必然往左就是关系选择器。关系选择器会让引擎去选其兄长或父亲(具体参见Sizzle.selector.relative下的四大函数),把这些元素置换到候选集对等的位置上。而后到下一个选择器组时,就是纯过滤操做。主过滤函数sizzle.filter会调用sizzle.seletors下N个过滤函数对这些元素进行监测,将不符合的元素替换为false.所以,到最后去重排序时,映射集是一个包含布尔值与元素节点的数组(true也是在这个步奏中产生的)。
种子集是分两步选择出来的。 首先,经过Sizzle.find获得一个大体的结果。而后经过Sizzle.filter,传入最右那个选择器组剩余的部分作参数,缩小范围。
//这是征对最左边的选择器组存在ID作出的优化 ret = Sizzle.find(parts.shift(), context, contextXML); context = ret.expr ? Sizzle.filter(ret.expr, ret.set)[0] : ret.set[0] ret = seed ? { expr : parts.pop(), set : makeArray(seed) //这里会对~,+进行优化,直接取它的上一级作上下文 } : Sizzle.find(parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+" ) && context.parentNode ? context.parentNode : context, contextXML); set = ret.expr ? Sizzle.filter(ret.expr, ret.set) : ret.set;
咱们是先取span仍是取.aaa呢?这里有个准则,确保咱们后面的映射集最小化。直白的说,映射集里边的元素越少,那么调用的过滤函数的次数就越少,说明进入另外一个函数做用域所形成的耗时就越少,从而总体提升引擎的选择速度。
为了达到此目的,这里作了一个优化,原生选择器的调用顺序被放到了一个叫Sizzle.selector.order的数组中,对于陈旧的浏览器,其顺序为ID,NANME,TAG,对于支持getElementByClassName的浏览器,其顺序为ID,Class,NAME,TAG。由于,ID至多返回一个元素节点,ClassName与样式息息相关,不是每一个元素都有这个类名,name属性带来的限制可能比className更大,但用到的概率较少,而tagName可排除的元素则更少了。
那么Sizzle.find就会根据上面的数组,取得它的名字,依次调用Sizzle.leftMatch对应的正则,从最右的选择器组切割下须要的部分,将换行符处理掉,经过四大查找函数获得一个粗糙的节点集合。若是获得"[href=aaa]:visible"这样的选择符,那么只有把文档中全部节点做为结果返回。
//sizzle.find为主查找函数 Sizzle.find = function(expr, context, isXML) { var set, i, len, match, type, left; if (!expr) { return []; } for (i = 0, len = Expr.order.length; i < len; i++) { type = Expr.order[i]; //读取正则,匹配想要的id class name tag if ((match = Expr.leftMatch[type].exec(expr))) { left = match[1]; match.splice(1, 1); //处理换行符 if (left.substr(left.length - 1) !== "\\") { match[1] = (match[1] || "").replace(rBackslash, ""); set = Expr.find[type] (match, context, isXML); //若是不为undefined , 那么取得选择器组中用过的部分 if (set != null) { expr = expr.replace(Expr.match[type], ""); break; } } } } if (!set) { //没有的话,寻找该上下文对象的全部子孙 set = typeof context.getElementsByTagName !== "undefined" ? context.getElementsByTagName("*") : []; } return { set: set, expr : expr }; };
通过主查找函数处理后,咱们获得一个初步的结果,这时最右边的选择器可能还有残余,好比“div span.aaa”可能余下"div span","div .aaa.bbb"可能余下“div .bbb”,这个转交主过滤函数Sizzle.filter函数处理。
它有两种不一样的功能,一是不断的缩小集合的个数,构成种子集返回。另外一种是将原集合中不匹配的元素置换为false。这个根据它的第三个传参inplace而定。
Sizzle.filter = function (expr, set, inplace, not) { //用于生成种子集或映射集,视第三个参数而定 //expr: 选择符 //set: 元素数组 //inplace: undefined, null时进入种子集模式,true时进入映射集模式 //not: 一个布尔值,来源自去反选择器 ..... }
待咱们把最右边的选择器组的最后都去掉后,种子集宣告完成,而后处理下一个选择器组,并将种子集复制一下,生成映射集。在关系选择器4个对应函数———他们为Sizzle.selectors.relative命名空间下————只是将映射集里边的元素置换为它们的兄长父亲,个数是不变。所以映射集与种子集的数量老是至关。另外,这四个函数内部也在调用Sizzle.filter函数,它们的inplace参数为true,走映射集的逻辑。
若是存在并联选择器,那就再调用Sizzle主函数,把获得两个结果合并去重
if (extara) { Sizzle(extra, origContext, results, seed); Sizzle.uniqueSort(result) }
这个过程就是Sizzle的主流程。下面将是根据浏览器的特性优化或调整的部分。好比ie6 7下的getElementById有bug,须要冲洗Expr.find.ID与Expr.filterID.ie6-ie8下,Array.prototype.slice.call没法切割NodeList,须要从写makeArray.IE6-8下,getElementsByTagName("*")会混杂在注释节点,须要从写Expr.find.TAG,若是浏览器支持querySelectorAll,那么须要重写整个Sizzle.
下面就从写个浏览器支持querySelectorAll的方法把:
if (document.querySelectorAll) { //若是支持querySelectorAll (function(){ var oldSizzle = Sizzle, div = document.createElement("div"), id = "__sizzle__"; div.innerHTML = "<p class='TEST'>test</p>"; //safari在怪异模式下querySelectorAll不能工做,终止从写 if (div.querySelectorAll && div.querySelectorAll(".TEST").length === 0) { return; } Sizzle = function(query, context, extra, seed) { context = context || document; //querySelectorAll只能用于HTML文档,在标准浏览器XML文档中实现了接口,但不工做 if (!seed && !Sizzle.isXML(context)) { //See if we find a selector to speed up var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(query); if (match && (context.nodeType === 1 || context.nodeType === 9)) { //元素Element文档Document //优化只有单个标签选择器的状况 if (match[1]) { return makeArray(context.getElementsByTagName(query), extra); //优化只有单个类选择的状况 } else if (match[2] && Expr.find.CLASS && context.getElementsByClassName) { return makeArray(context.getElementsByClassName(match[2]), extra); } } if (context.nodeType === 9) { //文档Document //优化选择符为body的状况 //由于文档只有它一个标签,而且对于属性直接取它 if (query === "body" && context.body) { return makeArray ([context.body], extra); //优化只有ID选择器的状况 //speed-up: Sizzle("ID") } else if (match && match[3]) { var elem = context.getElementById(match[3]); //注意,浏览器也会优化,它会缓存了上次的结果 //即使他如今移除了DOM树 if (elem && elem.parentNode) { //ie和opera会混淆id和name,确保id等于目标值 if (elem.id === match[3]) { return makeArray([elem], extra); } } else { return makeArray([], extra); } } try { return makeArray(context.querySelectorAll(query), extra); } catch (queryError) {} //ie8下的querySelectorAll实现存在bug,它会包含本身的集合内查找符合本身的元素节点 //根据规范,应该是在当前上下文中的全部子孙元素下查找,IE8下若是元素节点为Object,没法查找元素 } else if (context.nodeType === 1 && context.nodeName.toLowerCase() !== "object") { var oldContext = context; old = context.getElementById("id"), nid = old || id, hasParent = context.parentNode, relativeHierarchySelector = /^\s*[+~]/.test(query); if (!old) { context.setAttribute("id" , nid); } else { nid = nid.replace(/'/g, "\\$&"); } if (relativeHierarchySelector && hasParent) { context = context.parentNode; } // 若是存在id ,则将id取出来放到这个分组的最前面,好比div b --> [id=xxx] div b //不存在id,就建立一个,重复上面的操做,最后会删掉这个id try { if (!relativeHierarchySelector || hasParent) { return makeArray(context.querySelectorAll("[id='" + nid +"']" + query), extra); } } catch (pseudoError) {} finally { if (!old) { oldContext.removeAttribute("id"); } } } } return oldSizzle(query, context, extra, seed); }; //将原来的方法从新绑定到Sizzle函数上 for (var prop in oldSizzle) { Sizzle[prop] = oldSizzle[prop]; } //release memory in IE div = null })(); }
从源码中能够看出,它不仅仅是重写那么简单,根据不一样的状况还有各类提速方案。getElementById自不用说,速度确定快,这内部作了缓存,并且getElementById最多只返回一个元素节点,而querySelectorAll则会返回拥有这个ID值的多个元素。这个听起来有点奇怪,querySelectorAll不会理会你的错误行为,机械执行指令。
另外getElementsByTagName也是内部使用了缓存,它也比querySelectorAll快,getElementsByTagName返回的是一个NodeList对象,而querySelectorAll返回的是一个StaticNodeList对象。一个是动态的,一个是静态的。
测试下不一样:
var tag = "getElementsByTagName", sqa = "querySelectorAll"; console.log(document[tag]("div") == document[tag]("div")); //=>true console.log(document[sqa]("div") == document[sqa]("div")); //=>false
ps:true意味着它们拿到的同是cache引用,Static每次返回都是不同的Object
往上有数据代表,建立一个动态的NodeList对象比一个静态的StaticNodeList对象快90%
querySelectorAll的问题远不至与此,IE8中微软抢先实现了它,那时征对它的规范尚未完成,所以不明确的地方微软自行发挥了。IE8下若是对StaticNodeList取下标超界,不会返回undefined,而是抛出异常 Invalid procedure call or argument。
var els = document.body.querySelectorAll('div'); alert(els[2])//2> els.length-1
所以,一些奇特的循环,要适可而止。
最后,说明下querySelectorAll这个API在不一样的浏览器。不一样的版本中都存在bug,不支持的选择器类型多了,咱们须要在工做中作充分的测试(Sizzle1.9中,中枪的伪类就有focus , :enabled , :disabled , :checked.....)。
在现实工做中,想要支持选择器类型越多,就须要在结构上设计的有扩展性。但过度添加直接的定义伪类,就意味着将来与querySelectorAll发生的冲突越多。像zopto.js,就是一个querySelectorAll当成本身选择器的引擎,Sizzle1.9时已经有1700行代码。最近jQuery也作了一个selector-native模块,审视将来。
(本文完结,欢迎关注下章内容)