第七章:选择器引擎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,我们查找元素就非常方便。如下表
查找元素 遍历所有子节点遍历所有子元素
第一个firstChildfirstElementChild
最后一个lastChildlastElementChild
前面的previousSiblingpreviousElementSibling
后面的nextSiblingnextElementSibling
父节点parentNodeparentElement
数量 lengthchildElementCount
(本文尚未完结,由于篇幅较长,请关注更新)
即将更新:
伪类
(1).动作伪类
(2).目标伪类
(3).语言伪类
(4).状态伪类
(5).结构伪类
(6).去反伪类
(7).引擎实现时涉及的概念
4.选择器引擎涉及的通用函数
5.sizzle引擎
上一章:第六章
第六章:类工厂 下一章:第八章:节点模块
每篇文章都是在工作之余写的,分享是快乐!欢迎大家拍砖和点赞。(关注我即可随时关注更新)
一:javascript基础系列(已完结)二:javascript基础系列之DOM(已完结) 三:jQuery系列文章(已完结)四:AJAX(已不更新)五:JavaScript权威指南(核心篇,已完结)六:JavaScript框架设计(未完结)(个人博客网址,欢迎参观友情互链)
http://study.ahthw.com分类:
javaScript框架设计