6.4.4 切割器

选择器降低了JavaScript的入行门槛,它们在选择元素时都很随意,随心所欲,一级级地往上加ID类名,导致选择符非常长。因此如果不支持querySelectorAll,没有一个原生API能承担这工作。因此我们通常使用正常对用户的选择符进行切割。这个步骤有点像编译原理的词法分析,拆分出有用的符号法出来。

这里就拿Icarus的切割器来举例,看它是怎么一步步进化,就知道这工作需要多少细致。

最早的版本如图6.2所示。

▲图6.2

比如,对于“.td1,div a,body”,上面的正则可以完美将它分解为如下数组。

      [".td1",",","div"," ", "*", ",", "body"]

然后我们就可以根据这个符号流进行工作。由于没有指定上下文对象,就从document开始,发现第一个是类选择器,可以用getElementsByClassName,如果没有原生的,我们仿造一个也不是难事。然后是并联选择器,将上面得到的元素放进结果集。接着是标签选择器,使用getElementsByTagName。接着发现是后代选择器,这里可以优化,我们可以预先查看下一个选择器群组是什么,发现是通配符选择器,因此继续使用getElementsByTagName。接着又是并联选择器,将上面结果放入结果集。最后一个是标签选择器,又使用getElementsByTagName。最后是去重排序。

显然有了切割好的符号,工作简单多了。

但没有东西一开始就是完美的,比如我们遇到这样一个选择符:":nth-child(2n+1)"。这是一个单独的子元素过滤伪类,它不应该在这里被分析。后面有专门的正则对它的伪类名与传参进行处理。在切割器里,它能得到的最小词素是选择器!

于是切割器改进如下。

我们不断增加测试样例,就会发现越来越多问题。又如这样一个选择符:".td1[aa='>111']",属性选择器被拆碎了!

      [".td1","[aa",">","111"]

正则改进如下。

对于符择符“td + div span”,如果最后有一大堆空白,会导致解析错误,我们确保后代选择器夹在两个选择器之间。

      ["td", "+", "div"," ","span", " "]

最后一个选择器会被我们的引擎认作是后代选择器,需要提前去掉。

如果我们也想把最前面的空白去掉,那可能不是单独一个正则能做到的。现在切割器已经被我们搞得相当复杂了,维护性很差。在 mootools 等引擎中,里面的正则表达式更加复杂,可能是用工具生成的。到了这个地步,我们就需要转换思路,将切割器改为一个函数处理。当然,它里面也少不了正则表达式。正则是处理字符串的利器。

      var reg_split =
      /^[\w\u00a1-\uFFFF\-\*]+|[#.:][\w\u00a1-\uFFFF-]+(?:\([^\])*\))?|\[[^\]]*\]|(?:\s*)
      [>+~,](?:\s*)|\s(?=[\w\u00a1-\uFFFF*#.[:])|^\s+/;
      var slim = /\s+|\s*[>+~,*]\s*$/
      function spliter(expr) {
          var flag_break = false;
          var full = [];//这里放置切割单个选择器群组得到的词素,以“,”为界
          var parts = [];//这里放置切割单个选择器组得到的词素,以关系选择器为界
          do {
              expr = expr.replace(reg_split, function(part) {
                  if (part === ",") {//这个切割器只处理到第一个并联选择器
                      flag_break = true;
                  } else {
                      if (part.match(slim)) {//对关系并联。通配符选择器两边的空白进行处理
                          //对parts进行反转,因为div.aaa,反转后先处理.aaa
                          full = full.concat(parts.reverse(), part.replace(/\s/g, ''));
                          parts = [];
                      } else {
                          parts[parts.length] = part;
                      }
                  }
                  return "";//去掉已经处理了的部分
              });
              if (flag_break)
                  break;
          } while (expr)
          full = full.concat(parts.reverse());
          !full[0] && full.shift();//去掉开头第一个空白
          return full;
      }
      var expr = "  div  >  div#aaa,span"
      console.log(spliter(expr));//["div",">","#aaa", "div"]

当然,这个相对于Sizzle1.8与Slick等引擎的切割器来说,不值一提。写好一个切割器,需要有非常深厚的正则表达式功力。深层的知识包括自动机理论。