IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    第七章:选择器引擎 - 村长很忙

    村长很忙发表于 2015-06-30 17:23:00
    love 0
    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*/functiongetAllchildren(e){//取得一个元素的子孙,并兼容ie5returne.all ? e.all : e.getElementsByTgaName('*');}document.getElementsBySelector=function(selector){//如果不支持getElementsByTagName 则直接返回空数组if(!document.getElementsByTgaName) {returnnewArray();}//切割CSS选择符,分解一个个单元格(每个单元可能代表一个或多个选择器,比如p.aaa则由标签选择器和类选择器组成)vartokens = selector.split(' ');varcurrentContext =newArray(document);//从左至右检测每个单元,换言此引擎是自顶向下选择元素//如果集合中间为空,立即中至此循环for(vari = 0 ; i < tokens.length; i++) {//去掉两边的空白(并不是所有的空白都没有用,两个选择器组之间的空白代表着后代迭代器,这要看作者们的各显神通)token = tokens[i].replace(/^\s+/,'').replace(/\s+$/,'');//如果包含ID选择器,这里略显粗糙,因为它可能在引号里边。此选择器支持到属性选择器,则代表着可能是属性值的一部分。if(token.indexOf('#') > -1) {//假设这个选择器是以tag#id或#id的形式,可能导致bug(但这些暂且不谈,沿着作者的思路看下去)varbits =token.split('#');vartagName = bits[0];varid = bits[1];//先用id值取得元素,然后判定元素的tagName是否等于上面的tagName//此处有一个不严谨的地方,element可能为null,会引发异常varelement =document.getElementById(id);if(tagName && element.nodeName.toLowerCase() !=tagName) {//没有直接返回空结合集returnnewArray();}//置换currentContext,跳至下一个选择器组currentContext =newArray(element);continue;}//如果包含类选择器,这里也假设它以.class或tag.class的形式if(token.indexOf('.') > -1){varbits = token.split('.');vartagName = bits[0];varclassName = bits[1];if(!tagName){tagName= '*';}//从多个父节点,取得它们的所有子孙//这里的父节点即包含在currentContext的元素节点或文档对象varfound =newArray;//这里是过滤集合,通过检测它们的className决定去留varfoundCount = 0;for(varh = 0; h < currentContext.length; h++){varelements;if(tagName == '*'){elements=getAllchildren(currentContext[h]);}else{elements=currentContext[h].getElementsByTgaName(tagName);}for(varj = 0; j < elements.length; j++) {found[foundCount++] =elements[j];}}currentContext=newArray;for(vark = 0; k < found.length; k++) {//found[k].className可能为空,因此不失为一种优化手段,但new regExp放在//外围更适合if(found[k].className && found[k].className.match(newRegExp('\\b'+className+'\\b'))){currentContext[currentContextIndex++] =found[k];}}continue;}//如果是以tag[attr(~|^$*)=val]或[attr(~|^$*)=val]的组合形式if(token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/)){vartagName = RegExp.$1;varattrName = RegExp.$2;varattrOperator = RegExp.$3;varattrValue = RegExp.$4;if(!tagName){tagName= '*';}//这里的逻辑以上面的class部分相似,其实应该抽取成一个独立的函数varfound =newArray;varfoundCount = 0;for(varh = 0; h < currentContext.length; h++){varelements;if(tagName == '*') {elements=getAllchildren(currentContext[h]);}else{elements=currentContext[h].getElementsByTagName(tagName);}for(varj = 0; j < elements.length; j++) {found[foundCount++] =elements[j];}}currentContext=newArray;varcurrentContextIndex = 0;varcheckFunction;//根据第二个操作符生成检测函数,后面的章节有详细介绍 ,请继续关注哈switch(attrOperator) {case'=' ://checkFunction =function(e){return(e.getAttribute(attrName) ==attrValue);};break;case'~':checkFunction=function(e){return(e.getAttribute(attrName).match(newRegExp('\\b' +attrValue+ '\\b')));};break;case'|':checkFunction=function(e){return(e.getAttribute(attrName).match(newRegExp('^'+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) {returne.getAttribute(attrName);};}currentContext=newArray;varcurrentContextIndex = 0;for(vark = 0; k < found.length; k++) {if(checkFunction(found[k])) {currentContext[currentContextIndex++] =found[k];}}continue;}//如果没有 # . [ 这样的特殊字符,我们就当是tagNamevartagName =token;varfound =newArray;varfoundCount = 0;for(varh = 0; h < currentContext.length; h++) {varelements =currentContext[h].getElementsByTgaName(tagName);for(varj = 0; j < elements.length; j++) {found[foundCount++] =elements[j];}}currentContext=found;}returncurrentContext;//返回最后的选集} 显然当时受网速限制,页面不会很大,也不可能有很复杂的交互,因此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+ functiongetChildren(el) {if(el.childElementCount) {return[].slice.call(el.children);}varret =[];for(varnode = el.firstChild; node; node =node.nextSibling) {node.nodeType== 1 &&ret.push;(node);}returnret;}相邻选择器: 就是取得当前元素向右的一个元素节点,视情况使用nextSibling或nextElementSibling.functiongetNext (el) {if("nextElementSibling"inel) {returnel.nextElementSibling}while(el =el.nextSibling) {if(el.nodeType === 1) {returnel;}}returnnull}兄长选择器:就是取其右边的所有同级元素节点。functiongetPrev(el) {if("previousElementSibling"inel) {returnel.previousElementSibling;}while(el =el.previousSibling) {if(el.nodeType === 1) {returnel;}}returnnull;}上面提到的childElementCount 、 nextElementSibling是08年12月通过Element Traversal规范的,用于遍历元素节点。加上后来补充的parentElement,我们查找元素就非常方便。如下表查找元素 遍历所有子节点遍历所有子元素第一个firstChildfirstElementChild最后一个lastChildlastElementChild前面的previousSiblingpreviousElementSibling后面的nextSiblingnextElementSibling父节点parentNodeparentElement数量lengthchildElementCount(本文尚未完结,由于篇幅较长,请关注更新)即将更新:伪类(1).动作伪类(2).目标伪类(3).语言伪类(4).状态伪类(5).结构伪类(6).去反伪类(7).引擎实现时涉及的概念4.选择器引擎涉及的通用函数5.sizzle引擎上一章:第六章 第六章:类工厂 下一章:第八章:节点模块本文链接:第七章:选择器引擎,转载请注明。


沪ICP备19023445号-2号
友情链接