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

6.2 getElementsBySelector

我们先来看一下这个最古老的选择器引擎。它规定了今后许多选择器的发展方向。在解读中可能涉及许多概念,但不要紧,后面有更详细的解析。现在只是初步了解一下大概蓝图。

      /* document.getElementsBySelector(selector)
        New in version 0.4: Support for CSS2 and CSS3 attribute selectors:
        See http://www.w3.org/TR/css3-selectors/#attribute-selectors
              Download by http://www.bvbsoft.com
        Version 0.4 - Simon Willison, March 25th 2003
        -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer
      5 on Windows
        -- Opera 7 fails
      */
        //
      function getAllChildren(e) {
        //取得一个元素的所有子孙,兼并容IE5
        return e.all ? e.all : e.getElementsByTagName('*');
      }
      document.getElementsBySelector = function(selector) {
        //如果不支持getElementsByTagName则直接返回空数组
        if (!document.getElementsByTagName) {
          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].getElementsByTagName(tagName);
              }
              for (var j = 0; j < elements.length; j++) {
                found[foundCount++] = elements[j];
              }
              }
              currentContext = new Array;
              var currentContextIndex = 0;
              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
          tagName = token;
          var found = new Array;
          var foundCount = 0;
          for (var h = 0; h < currentContext.length; h++) {
              var elements = currentContext[h].getElementsByTagName(tagName);
              for (var j = 0; j < elements.length; j++) {
              found[foundCount++] = elements[j];
              }
          }
          currentContext = found;
        }
        return currentContext;//最后返回结果集
      }

显然受当时的网速限制,页面不会很大,也不可能发展起复杂的交互,因此JavaScript还没有到大规模使用的阶段,我们看到那时的库也不怎么重视全局污染。主要API直接在document上操作,参数只有一个CSS表达符。从我们的分析来看,它不支持并联选择器(后面介绍),并且要求每个选择器组不能超出两个,否则报错。换言之,它只对下面这样形式的CSS表达式有效:

      #aa p.bbb [ccc=ddd]

CSS表达符将以空白分割成多个选择器组,每个选择器不能超过两种选择器类型,并且其中一种为标签选择器。

要求比较严格,文档也没有指明,因此非常糟糕。但对当时的编程环境来说,已经是喜出望外了。作为早期的选择器,它也没有像以后那样对结果集进行去重,把元素逐个按照文档出现的顺序进行排序。我们在第一节指出的Bug,它也没有规避,这可能受当时JavaScript技术交流太少所致。这些都是我们是日后要改进的地方。