JavaScript框架设计
上QQ阅读APP看书,第一时间看更新

6.3 选择器引擎涉及的知识点

这一节我们开始学习一下上节介绍的大量概念。其中,有关选择器引擎实现的概念大多数是我从Sizzle中抽取出来的,而CSS表达符部分则是W3C提供的。那么我们先从CSS表达符部分讲起吧。

选择符是指一条CSS样式规则的最左边的部分,如图6.1所示。

▲图6.1 CSS样式规则

上面的只是理想情况,重构人员交给我们的 CSS 文件,里面的选择符可是复杂多了。选择符混杂着大量的标记,可以分割为许多更细的单元。总的来说,分为四大类十七种。此外,还没有包含选择器引擎无法操作的伪元素。

四大类是指并联选择器、简单选择器、关系选择器与伪类。

并联选择器就是“,”,一种不是选择器的选择器,用于合并多个分组的结果。

简单选择器分五种:ID、标签、类、属性、通配符。

关系选择器分四种:亲子、后代、相邻、兄长。

伪类分六种:动作伪类、目标伪类、语言伪类、状态伪类、结构伪类、取反伪类。

简单选择器又称为基本选择器,这是在 Prototype.js 之前的选择器都已经支持的选择器类型。不过CSS上,IE7才支持部分属性选择器。其中,它们设计得非常整齐划一,我们可以通过它的第一个字符决定它们的类型。比如:ID选择器的第一个字符为“#”;类选择器为“.”;属性选择器为“[”;通配符选择器为“*”;标签选择器为英文字母,你也可以大概解释为没有特殊符号,jQuery就是使用/isTag = !/\W/.test( part )进行判定的。

在实现上,我们在这里有许多原生 API 可用,如 getElementById、getElementsByTagName、getElementsByClassName、document.all,属性选择器可以用getAttribute、getAttributeNode、attributes、hasAttribute, 2003年曾讨论引入getElementsByAttribute, 但没有成功,Firefix上XUI的同名API就是当时的产物。不过属性选择器的确比较复杂,历史上它是分两步实现的。

CSS2.1中,属性选择器有以下四种形态。

[att]:选取设置了att属性的元素,不管设定的值是什么。

[att=val]:选取所有att属性的值完全等于 val 的元素。

[att~=val]:表示一个元素拥有属性 att,并且该属性含有空格分隔的一组值,其中之一为 'val'。这个大家应该能联想到类名,如果浏览器不支持 geElementsByClassName,在过滤阶段,我们可以将.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, getElementsByTagName(“*”)混入注释节点的问题。

亲子选择器:这个我们如果不打算兼容XML,那么直接使用children就行了。不过IE5~IE8它都会混入注释节点。下面是兼容列表。

      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或nextElement Sibling。

      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是2008年12月通过Element Traversal规范的,用于遍历元素节点。加上后来补充的parentElement,我们查找元素就非常方便,如表6.1所示。

表6.1

伪类是选择器中最庞大的家族,从CSS1开始支持,以字符串开头。在CSS3,出现了要求传参的结构伪类与取反伪类。

1. 动作伪类

动作伪类又分为链接伪类和用户行为伪类,其中链接伪类由:visited和:link组成,用户行为伪类由:hover、:active和:focus组成。这里我们基本上只能模拟:link,而在浏览器原生的querySelectorAll对它们的支持也存在差异,IE8~IE10 取:link存在错误,它只能取A标签,实际:link是指代A、AREA、LINK这三种标签,这个其他标签浏览器都正确。另外,除Opera,Safari外,其他浏览器取:focus都正常,除Opera外,其他浏览器取:hover都正确。剩下的:active与:visited都为零。下面是测试页面。

      <!DOCTYPE HTML>
      <html>
          <head>
              <title></title>
              <link href="aa" type="text/css" rel="stylesheet" charset="utf-8" />
              <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
              <script>
                  window.onload = function() {
                      document.querySelector("#aaa").onclick = function() {
                          alert(document.querySelectorAll(":focus").length);//1
                      }
                      document.querySelector("#bbb").onmouseover = function() {
                          //4 html, body, p, a
                          alert(document.querySelectorAll(":hover").length);
                      }
                  }
                  function test() {
                      alert(document.querySelectorAll(":link").length);// 6
                  }
              </script>
          </head>
          <body>
              <p><a href="javascript:void 0" id="aaa">aaa</a></p>
              <p><a href="javascript:void 0" id="bbb">bbb</a></p>
              <button type="button" onclick="test()">点我</button>
              <img src="planets.jpg" border="0" usemap="#planetmap" alt="Planets" />
              <map name="planetmap" id="planetmap">
                  <area shape="circle" coords="180,139,14" href ="venus.html" alt="Venus" />
                  <area shape="circle" coords="129,161,10" href ="mercur.html" alt="Mercury" />
                  <area shape="rect" coords="0,0,110,260" href ="sun.html" alt="Sun" />
              </map>
          </body>
      </html>

伪类没有专门的 API 得到结果集,因此我们需要通过上一次得到的结果集进行过滤。在浏览器中,我们可以通过document.links得到部分结果,因为它不包含LINK标签。因此,最好的方法是判定它的tagName是否等于A、AREA、LINK的其中一个。

2.目标伪类

目标伪类即:target伪类,指其id或者name属性与URL中的hash部分(即#之后的部分)匹配上的元素。

譬如文档中有一个元素,其id为section_2,而URL中的hash部分也是#section_2,那么它就是我们要取的元素。

Sizzle中的过滤函数如下。

      "target": function(elem) {
          varhash = window.location && window.location.hash;
          return hash && hash.slice(1) === elem.id;
      },

3.语言伪类

语言伪类即:lang伪类,用来设置使用特殊语言的内容样式,如:lang(de)的内部应该为德语,需要特殊处理。

注意:lang 虽然作为 DOM 元素的一个属性,但:lang 伪类与属性选择器有所不同,具体表现在:lang伪类具有“继承性”,如下面HTML表示的文档。

      <body lang="de"><p>一个段落</p></body>

如果使用[lang=de]则只能选择到body元素,因为p元素没有lang属性。但是使用:lang(de)则可以同时选择到body和p元素,表现出继承性。

Sizzle中的过滤函数如下。

      "lang": markFunction(function(lang) {
          // lang value must be a valid identifider
          if (!ridentifier.test(lang || "")) {
              Sizzle.error("unsupported lang: " + lang);
          }
          lang = lang.replace(runescape, funescape).toLowerCase();
          return function(elem) {
              var elemLang;
              do {
                  if ((elemLang = documentIsXML ? elem.getAttribute("xml:lang") || elem.getAttri bute("lang") : elem.lang)) {
                      elemLang = elemLang.toLowerCase();
                      return elemLang === lang || elemLang.indexOf(lang + "-") === 0;
                  }
              } while ((elem = elem.parentNode) && elem.nodeType === 1);
              return false;
          };
      }),

对比mass的实现如下。

      lang: { //标准 CSS3语言伪类
          exec: function(flags, elems, arg) {
              var result = [],
                  reg = new RegExp("^" + arg, "i"),
                  flag_not = flags.not;
              for (var i = 0, ri = 0, elem; elem = elems[i++];) {
                  var tmp = elem;
                  while (tmp && !tmp.getAttribute("lang"))
                  tmp = tmp.parentNode;
                  tmp = !! (tmp && reg.test(tmp.getAttribute("lang")));
                  if (tmp ^ flag_not) result[ri++] = elem;
              }
              return result;
          }
      },

4.状态伪类

状态伪类用于标记一个 UI 元素的当前状态,由:checked、:enabled、:disabled 和:indeterminate这4个伪类组成。我们可以分别通过元素的checked、disabled、indeterminate属性进行判定。

5.结构伪类

它又可以分为三种,根伪类、子元素过滤伪类与空伪类。根伪类是由它在文档的位置判定,子元素过滤伪类是根据它在其父亲的所有孩子的位置或标签类型判定,空伪类是根据它孩子的个数判定。

:root伪类用于选取根元素,在HTML文档中通常是html元素。

:nth-child 是所有子元素过滤伪类的蓝本,其他 8 种都是由它衍生出来的。它带有参数,可以是纯数字、代数式或单词。如果是纯数字,数字是从1计起;如果是代数式,n则从零递增,非常不好理解的规则。下面是示例。

:nth-child(2)选取当前父节点的第2个子元素。

:nth-child(n+4)选取大于等于4标签,我们可以把n看成自变量(0 ≤ n ≤parent.children.length),此代数式的值为因变量。

:nth-child(−n+4)选取小于等于4标签。

:nth-child(2n)选取偶数标签,2n也可以是even。

:nth-child(2n1)选取奇数标签,2n1可以是odd。

:nth-child(3n+1)表示每三个为一组,取它的第一个。

:nth-last-child与:nth-child差不多,不过是从后面取起。比如:nth-last-child(2)。

:nth-of-type和:nth-last-of-type与:nth-child和:nth-last-child类似,规则是将当前元素的父节点的所有元素按照其tagName分组,只要其参数符合它在那一组的位置就被匹配到。比如:nth-of-type(2),具体如下。

另一个例子,:nth-of-type(even),具体如下。

:first-child用于选择第一个子元素,效果等同于:nth-child(1)。

:last-child用于选择最后一个子元素,效果等同于:nth-last-child(1)。

:first-of-type和:last-of-type等同于:nth-of-type(1)和:nth-last-of-type(1)。

:only-child用于选择唯一的子元素,当子元素个数超过1个时,选择器失效。

:only-of-type将父节点的子元素按tagName分组,如果某一组只有一个元素,那么就选择这些组的元素返回。

:empty用于选择那些不包含任何元素节点、文本节点、CDATA节点的元素,但允许里面只存在注释节点。

Sizzle中的实现如下。

      "empty": function(elem) {
          for (elem = elem.firstChild; elem; elem = elem.nextSibling) {
              if (elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4) {
              return false;
              }
          }
          return true;
      },

mootools中Slick.的实现如下。

      "empty": function(node) {
          var child = node.firstChild;
          return !(child && child.nodeType == 1) && !(node.innerText || node.textContent ||
      '').length;
      },

6. 取反伪类

取反伪类即:not伪类,其参数为一个或多简单选择器,里面用逗号隔开。在jQuery等选择器引擎中允许你传入其他类型的选择器,甚至可以进行多个取反伪类套嵌。

7. 引擎在实现时涉及的概念

种子集:或者叫候选集,如果CSS选择符非常复杂,我们要分几步才能得到我们想要的元素。那么第一次得到的元素集合就叫种子集。在Sizzle这样基本从右到左,它的种子集中就有一部分为我们最后得到的元素。如果选择器引擎是从左到右选择器,那么它们只是我们继续查它们的孩子或兄弟的“据点”而已。

结果集:选择器引擎最终返回的元素集合,现在约定俗成,它要保持与 querySelectorAll 得到的结果一致,即,没有重复元素,元素要按照它们在DOM树上出现的顺序排序。

过滤集:我们选取一组元素后,它之后的每一个步骤要处理的元素集合都可以称为过滤集。比如p.aaa,如果浏览器不支持querySelectorAll,若支持getElementsByClassName,那么我们就用它得到种子集,然后在循环中通过tagName==="P"进行过滤。若不支持,只能通过getElementsByTagName得到种子集,然后通过className进行过滤。显然大多数情况下,前者比后者快多了。同理,如果它们之间存在ID 选择器,由于ID 在一个文档中不允许重复,因此使用ID 进行查找更快。在Sizzle,如果不支持querySelectorAll,它会智能地以ID、Class、Tag的顺序进行查找。

选择器群组:一个选择符被并联选择器“,”划分成的每一个大分组。

选择器组:一个选择器群组被关系选择器划分的第一个小分组。考虑到性能,每一个小分组建议都加上tagName,因为这样在IE6会方便我们使用getElementsByTagName。比如p div.aaa 比p.aaa快多了。前者是通过两次 getElementsByTagName 寻找,最后用 className 过滤,后者是getElementsByTagName 得到种子集,然后再取它们的所有子孙,明显这样得到的过滤集比前者的数量多很多。

从实现上说,你可以选择从左到右,也可以像Sizzle那样从右到左,但Sizzle只能说大体上是这个方向,实际情况复杂多了。

另外,选择器也分为编译型与非编译型,编译型是EXT发明的,这个阵营的选择器中有EXT、QWrap、NWMatchers、JindoJS韩国人写的框架,http://jindo.dev.naver.com。非编译型的就更多了,如SizzleIcarus(mass Framework的选择器引擎)Slick(mootools的选择器)YUIdojouupaa、peppy……

还有一种利用xpath实现的选择器,最著名的是Base2。它先实现了xpath那一套,方便IE也能使用 document. evaluate,然后将 CSS 选择符翻译成 xpath。其他比较出名的如 casperjs、DOMAssistant。

像Sizzle、mootools、Icarus等还支持选择XML元素,因为XML还是一种重要的数据传输格式,后端通过XHR返回我们的可能就是XML,这样我们通过选择器引擎抽取所需要的数据就简单多了。