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等引擎的切割器来说,不值一提。写好一个切割器,需要有非常深厚的正则表达式功力。深层的知识包括自动机理论。