你的位置:首页 > Java教程

[Java教程]第七章:选择器引擎


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引入

微软为保住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  的("*")混入注释节点的问题。

亲子选择器:这个我们如果不打算兼容

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引擎

上一章:第六章 第六章:类工厂  下一章:第八章:节点模块