Sizzle 源码分析讨论(一): 正则表达式

708 阅读4分钟

前言

这是我第一篇文章,最近看司徒大大的《javascript框架设计》,看到了Sizzle这里,就想看看源码,上网找了文章版本都是1.7之前的,而且很零零散散。所以打算自己写一些文章,从头到尾把Sizzle读一遍。作为一个前端小白,肯定是有很多看不懂的,希望各位大佬能吹风磁暴帮帮我。我也在GitHub以注释的方式分析源码: 施工中。OK, 不废话了开始干。


正则表达式

正则分两种写法,一种是字面量;一种是用RegExp构造函数。当时用构造函数模式的时,所传入的字符串需要解析, 如:

//这是字面量
var reg1 = /\\/;
//这是构造函数
var reg2 = new RegExp('\\\\');

上面所示的两个正则是匹配的字符是相同的都是匹配一个\, 但是传入构造函数的字符串要4个\,原因是在传入构造函数时,需要解析一次。这里可以去看红宝书的正则那一章写的很清楚。使用这种解析模式的正则读起来会特别的麻烦。Sizzle用的就是这种解析。


Sizzle的正则

Sizze中的正则主要是为了匹配选择器,区分是什么类型的选择器,进行分类。它拆分了几个基础的正则字符串,并再最后进行组合拼接,然后通过构造函数,new出实例。

由于Sizzle的正则都是构造函数模式的,我会在它的代码下面注释一个字面量模式的方便阅读。


booleans
先从最简单的开始,booleans就是一堆或,这里应该是进一步匹配没有值的伪类。比如::checked :disabled,而不去匹配带值的,比如::nth-child(3)

booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|" +
	    "ismap|loop|multiple|open|readonly|required|scoped",

whitespace(空白)
这是Sizzle自己写的空白,与\s的区别是少了一个\v

这里为什么少一个\v,我不太清楚,如果有知道的大佬,请告知谢谢。

whitespace = "[\\x20\\t\\r\\n\\f]"

//字面量
copy_whilespace = '/[\x20\t\r\n\f]/'
  • \x20 表示ASCII 十六进制第20个字符 也就是空白
  • \t 表示制表符
  • \r 表示回车符
  • \n 表示换行符
  • \f 表示换页符

identifier(标识符)
这个应该算是sizzle正则表达式的核心了,它可以匹配.class中的class,可以匹配:nth-child(3)中的nth-child

identifier = "(?:\\\\[\da-fA-F]{1,6})" + whitespace +
    "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+"

//字面量
copy_identifier = '/(?:\\[\da-fA-F]{1,6}[\x20\t\r\n\f]?|\\[^\r\n\f]|[\w-]|[^\x00-\x7f])+/'

首先identifier是一个非获取捕获,identifier匹配四种情况。

  1. \\[\da-fA-F]{1,6}[\x20\t\r\n\f]?匹配的是 ASCII表。如\61, 之所以是{1,6},应该支持二进制。
  2. \\[^\r\n\f]匹配的是\加不是\r\n\f的任意可见或不可见字符, 如\a
  3. [\w-]匹配的是任意可见字符或-
  4. [^\x00-\x7f]匹配不是ASCII表中的所有字符

这意味着,像诸如#.()~=!$等等这些是不会被匹配到的。


attributes(属性)
attributes匹配的是属性选择器,可以匹配[attr], 可以匹配[attr $= 'val'],但是不能匹配[attr =],也不能匹配[attr.]

attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
	"*([*^$|!~]?=)" + whitespace +
	"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" +
	whitespace + "*\\]";

//字面量 由于把identifier和whitespace都替换成正则的话 实在是太长了 这里直接用变量名代替了
//为了好阅读, 换点行
copy_attributes = `\[whitespace*(identifier)
        (?:whitespace*([*^$|!~]?=)whitesapce*
            (?:
                '((?:\\.|[^\\'])*)'|
                "((?:\\.|[^\\\"])*)"|
                (identifier)
            )
        |)whitespace*\]`;

//真正的样子
true_attributes =  /\[[\x20\t\r\n\f]*((?:\\[\da-fA-F]{1,6}[\x20\t\r\n\f]?|\\[^\r\n\f]|[\w-]|[^\x00-\x7f])+)(?:[\x20\t\r\n\f]*([*^$|!~]?=)[\x20\t\r\n\f]*(?:'((?:\\.|[^\\'])*)'|"((?:\\.|[^\\"])*)"|((?:\\[\da-fA-F]{1,6}[\x20\t\r\n\f]?|\\[^\r\n\f]|[\w-]|[^\x00-\x7f])+))|)[\x20\t\r\n\f]*\]/

属性正则匹配两种状态的属性选择器:一种有等号的[attr=val],一种没有等号的[attr]。将这两个区分开的是最外层的非捕获组的“或|“。
属性正则比较复杂,但是如果按捕获组来看的话,就很清晰了。属性正则一共有五个捕获组,除了第一个属性名一定会有值之外,其余的捕获组需要满足一定的条件:

  • $1 (identifier): 属性名
  • $2 ([*^$|!~]?=)运算符。如果是[attr]这种情况的话,值为空
  • $3 '((?:\\.|[^\\'])*)'用单引号括起来的属性值[attr='val']val
  • $4 "((?:\\.|[^\\\"])*)"用双引号括起来的属性值[attr="val"]val
  • $5 (identifier)没有用引号括起来的值[attr=val]val

引号中匹配的值和没有引号匹配的值也是有区别的。如[class=".aa"]可以匹配,但是[class=.aa]就不可以。


pseudos(伪类)
pseudos匹配的是伪类选择器

pseudos = ":(" + identifier + ")(?:\\((" +
	"('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" +
	"((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" +
	".*" +
	")\\)|)";
	
//字面量
copy_pseudos = `:(identifier)
    (?:\((
        ('((?:\\.|[^\\'])*)'|"((?:\\.|[^\\"])*)")|
        ((?:\\.|[^\\()[\]]|attributes)*)|
        .*
    )\)|)`

//全部
true_pseudos = /:((?:\\[\da-fA-F]{1,6}[\x20\t\r\n\f]?|\\[^\r\n\f]|[\w-]|[^\x00-\x7f])+)(?:\((('((?:\\.|[^\\'])*)'|"((?:\\.|[^\\"])*)")|((?:\\.|[^\\()[\]]|\[[\x20\t\r\n\f]*((?:\\[\da-fA-F]{1,6}[\x20\t\r\n\f]?|\\[^\r\n\f]|[\w-]|[^\x00-\x7f])+)(?:[\x20\t\r\n\f]*([*^$|!~]?=)[\x20\t\r\n\f]*(?:'((?:\\.|[^\\'])*)'|"((?:\\.|[^\\"])*)"|((?:\\[\da-fA-F]{1,6}[\x20\t\r\n\f]?|\\[^\r\n\f]|[\w-]|[^\x00-\x7f])+))|)[\x20\t\r\n\f]*\])*)|.*)\)|)/

伪类也有两种情况,一种:after,另一种是:nth-child(3)。伪类有11个捕获组,但是$7-$11都是attributes的。
这里只看前6个捕获组:

  • $1 (identifier)伪类名。如 :nth-child(3)中的nth-child, :after中的after
  • $2 \((....)\)最外层的捕获组, 捕获括号内所有的内容。如:nth-child(3)中的3:nth-child("3")中的"3":nth-child('3') 中的'3':not(".className")中的".className"
  • $3 ('((?:\\.|[^\\'])*)'|"((?:\\.|[^\\"])*)")小括号内使用引号的字符串。如:nth-child("3")中的"3":nth-child('3')中的'3'
  • $4 ((?:\\.|[^\\'])*)单引号中的值。如:nth-child('3')中的3
  • $5 ((?:\\.|[^\\"])*)双引号中的值。如:nth-child("3")中的3
  • $6 ((?:\\.|[^\\()[\]]|attributes)*)除了()[]\的字符串或者属性。如:nth-child(3)中的3:not([href = 'aaa'])中的[href = 'aaa']

通常情况下$3$6,这两个捕获组其中之一都必然有值。
但是有一种特殊情况:,:not(:nth-child(3)),这种情况下$3$6都是没有值的。Sizzle通过$6是否有值,区分了一些场景

生成正则实例


var rwhitespace = new RegExp( whitespace + "+", "g" ),  //空白
    rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), //前后留白
    rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),  //逗号
    rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ),   //组合器
    rdescend = new RegExp( whitespace + "|>" ), //后代
    rpseudo = new RegExp( pseudos ), //伪类
    ridentifier = new RegExp( "^" + identifier + "$" ), //标识码
    matchExpr = {
		"ID": new RegExp( "^#(" + identifier + ")" ),
		"CLASS": new RegExp( "^\\.(" + identifier + ")" ),
		"TAG": new RegExp( "^(" + identifier + "|[*])" ),
		"ATTR": new RegExp( "^" + attributes ),
		"PSEUDO": new RegExp( "^" + pseudos ),
		/**
		 /^:(only|first|last|nth|nth-last)-(child|of-type)
		    (?:\(whitespace*(
		            even|odd|(([+-]|)(\d*)n|)whitespace*
		            (?:([+-]|)whitespace*(\d+)|)
		    )whitespace*\)|)/i
		 // $1 	only|first|last|nth|nth-last
		 // $2	child|of-type
		 // $3	括号中全部的内容 如 2n+1、2n、1、even、odd...
		 // $4  表达式2n+1 中的2n  如果为even 或者 odd的话 这里为undefined
		 // $5	2n的正负 +2n -2n 中的 + -
		 // $6	n的倍数 2n 中的 2
		 // $7	运算符 + -
		 // $8	最后一个数 1
	     */
		"CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" +
			whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" +
			whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
		"bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
		"needsContext": new RegExp( "^" + whitespace +
			"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace +
			"*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
	},
	rhtml = /HTML$/i,
	rinputs = /^(?:input|select|textarea|button)$/i,
	rheader = /^h\d$/i, //h1 h2 h3 h4 h5 h6..
	rnative = /^[^{]+\{\s*\[native \w/,
	rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,    //快速寻找ID CLASS 标签
	rsibling = /[+~]/;  //兄弟
    

以上就是Sizzle全部的正则