jQuery凭借选择器风靡全球,各大框架类库都争先开发自己的选择,一时间内选择器变为框架的标配
早期的JQuery选择器和我们现在看到的远不一样。最初它使用混杂的xpath语法的selector。
第二代转换为纯css的自定义伪类,(比如从xpath借鉴过来的位置伪类)的sizzle,但sizzle也一直在变,因为他的选择器一直存在问题,一直到JQuery1.9才搞定,并最终全面支持css3的结构伪类。
2005 年,Ben Nolan的Behaviours.js 内置了闻名于世的getElementBySelector,是第一个集成事件处理,css风格的选择器引擎与onload处理的类库,此外,日后的霸主prototype.js页再2005年诞生。但它勉强称的上是,选择器$与getElementByClassName在1.2出现,事件处理在1.3,因此,Behaviour.js还风光一时。
本章从头至尾实验制造一个选择器引擎。再次,我们先看看前人的努力:
1.浏览器内置寻找元素的方法
请不要追问05年之前开发人员是怎么在这种缺东缺西的环境下干活的。那时浏览器大战正酣。程序员发明navugator.userAgent检测进行"自保"!网景战败,因此有关它的记录不多。但IE确实留下不少资料,比如取得元素,我们直接可以根据id取得元素自身(现在所有浏览器都支持这个特性),不通过任何API ,自动映射全局变量,在不关注全局污染时,这是个很酷的特性。又如。取得所有元素,使用document.All,取得某一种元素的,只需做下分类,如p标签,document.all.tags("p")。
有资料可查的是 getElementById , getElementByTagName是ie5引入的。那是1999年的事情,伴随一个辉煌的产品,window98,捆绑在一起,因此,那时候ie都倾向于为IE做兼容。
(感兴趣的话参见让ie4支持getElementById的代码,此外,还有getElementByTagsName的实现)
但人们很快发现问并无法选取题了,就是IE的getElementById是不区分表单元素的ID和name,如果一个表单元素只定义name并与我们的目标元素同名,且我们的目标元素在它的后面,那么就会选错元素,这个问题一直延续到ie7.
IE下的getElementsByTagesName也有问题。当参数为*号通配符时,它会混入注释节点,并无法选取Object下的元素。
(解决办法略去)
此外,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表示的文档
一个段落
如果使用[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:比较那些边界点:为常数
-
- Range.START_TO_START 比较两个Range节点的开始点
- Range.END_TO_END 比较两个Range节点的结束点
- Range.START_TO_END 用sourceRange的开始点与当前范围的结束点比较
- Range.END_TO_START 用sourceRange的结束点与当前范围的开始点做比较
特别的情况发生于要兼容旧版本标准浏览器与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='']")); //
console.log(document.querySelector("#test1[title~='']")); // null
console.log(document.querySelector("#test1[title|='']")); //
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 = "test
"; //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模块,审视未来。
(本文完结,欢迎关注下章内容)
上一章:第六章 第六章:类工厂 下一章 :第八章:节点模块