第七章:选择器引擎

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

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

时间: 2024-10-13 11:53:11

第七章:选择器引擎的相关文章

Unity3D入门 到精通 第七章物理引擎应用小结

实践了书上第七章的CrazyBall, 1.Camera的尾随,本例中没有使用已有的CarmeraFollow脚本,而是直接根据Ball的pos来跟随,当然也使用了插值,并设置了阻尼弹簧方式: transform.position = Vector3.Lerp(transform.position, newPos, Time.deltaTime * dampSpeed); 2.单例模式在游戏Manger管理中的应用: public static myCrazyBallManager mCB;//

使用JQuery快速高效制作网页交互特效第二章到第七章

第二章 JavaScript对象 浏览器对象模型(BOM)是JavaScript的组成之一,window对象是整个BOM的核心 window对象的常用方法 prompt():显示可提示用户输入的对话框 alert():显示一个带有提示信息和一个"确定"的按钮的警示对话框 confirm():显示一个滴啊有提示信息,"确定"和"取消"按钮的对话框 close():关闭浏览器窗口 open():打开一个新的浏览器窗口,加载给定URL制定的文档 set

2017.2.21 activiti实战--第七章--Activiti与容器集成

学习资料:<Activiti实战> 第七章 Activiti与容器集成 本章讲解activiti-spring可以做的事情,如何与现有系统集成,包含bean的注入.统一事务管理等. 7.1 流程引擎工厂 7.1.1 ProcessEngine 创建processEngine的方法有三种: 1 通过配置文件 2 测试中通过ActivitiRule 3 通过ProcessEngines类 7.1.2 ProcessEngineFactory 与spring集成的目的有两个: 1 通过spring统

语法》第七章 函数

(本文为阮一峰js标准教程的学习笔记,旨在总结该教程中涉及的知识点大纲及个人所做的一些拓展,方便作为"目录"或者"大纲"复习和查漏补缺,详细内容请参见阮一峰教程原文) 第二部分 语法 ************第七章 函数************ 一.概述函数就是一段可以反复调用的代码块.函数还能接受输入的参数,不同的参数会返回不同的值.1.函数的三种声明方法1.1function命令[标准函数声明方法]function 函数名(传入参数){函数体} 1.2采用函数表

[GEiv]第七章:着色器 高效GPU渲染方案

第七章:着色器 高效GPU渲染方案 本章介绍着色器的基本知识以及Geiv下对其提供的支持接口.并以"渐变高斯模糊"为线索进行实例的演示解说. [背景信息] [计算机中央处理器的局限性] 在大学的"数字图像处理"课程中,老师解说了高斯模糊的基本算法.并使用C#进行了基本实现.高斯模糊.简单地说,就是使用高斯权重模板对图像的每个像素进行再计算.填充,以达到模糊的效果. 在课程中.对于给定的模板与模糊度系数,对一副800X600的图像进行模糊处理.须要计算48万个像素点,

《精益创业》第七章读后感

在经历了创业公司失败之后,有了大量的闲时间,看书,思考,认真反思过去三年的所做所为,好多问题,在精益创业中都找到了答案. 当读到第七章时,真的后悔没有早点遇到这本书,如果能早点遇到这本书,也许公司会是另一番情景. 1.新创公司该不该抱定宗旨不动摇呢? <精益创业>中说:锲而不舍的传说是非常危险的. 的确,当我们在面临这样的局面的时候,我们选择了硬扛,其实是不知道该如何突破 2.如何判别转型的时机? <精益创业>中说:完成阶段性的目标,比如收入数字上升等,但这并不是衡量新创企业是否取

[GEiv]第七章:着色器 高效的GPU渲染方案

第七章:着色器 高效的GPU渲染方案 本章介绍着色器的基本知识以及Geiv下对其提供的支持接口,并以"渐变高斯模糊"为线索进行实例的演示讲解. [背景信息] [计算机中央处理器的局限性] 在大学的"数字图像处理"课程中,老师讲解了高斯模糊的基本算法,并使用C#进行了基本实现.高斯模糊,简单地说,就是使用高斯权重模板对图像的每一个像素进行再计算.填充,以达到模糊的效果. 在课程中,对于给定的模板与模糊度系数,对一副800X600的图像进行模糊处理,需要计算48万个像素

[书籍翻译] 《JavaScript并发编程》第七章 抽取并发逻辑

本文是我翻译<JavaScript Concurrency>书籍的第七章 抽取并发逻辑,该书主要以Promises.Generator.Web workers等技术来讲解JavaScript并发编程方面的实践. 完整书籍翻译地址:https://github.com/yzsunlei/javascript_concurrency_translation .由于能力有限,肯定存在翻译不清楚甚至翻译错误的地方,欢迎朋友们提issue指出,感谢. 到本书这里,我们已经在代码中明确地模拟了并发问题.使

第七章

第七章 控制发光二极管. 尽管linux 驱动直接和硬件打交道,但并不是linux驱动直接向硬件中的内存写数据,而是与本机的i/o内存进行交互.所谓I/O内存是通过各种接口(PCI, USB.蓝牙以太网等)连接到主机的硬件在主机的内存映射.Linux内核提供了多个与I/O内存交互的函数.Linux内核的内存管理模块负责同步I/O内存与硬件的数据. 每一个连接Linux 的硬件在I/O内存中都会有映射首地址.在使用ioread 32.ioread32等函数读写I/O内存时需要指定这些首地址.Led