分享

第七章:选择器引擎

 昵称10504424 2015-07-01

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

本文尚未完结,由于篇幅较长,请关注更新

即将更新

伪类
(1).动作伪类
(2).目标伪类
(3).语言伪类
(4).状态伪类
(5).结构伪类
(6).去反伪类
(7).引擎实现时涉及的概念

4.选择器引擎涉及的通用函数
5.sizzle引擎

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

每篇文章都是在工作之余写的,分享是快乐!欢迎大家拍砖和点赞。(关注我即可随时关注更新)
一:javascript基础系列(已完结) 二:javascript基础系列之DOM(已完结)  三:jQuery系列文章(已完结)
四:AJAX(已不更新) 五:JavaScript权威指南(核心篇,已完结) 六:JavaScript框架设计(未完结)
(个人博客网址,欢迎参观友情互链)http://study.

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约