这一篇我要演示如何实现一个选择器引擎。实现一个选择器比想象中的要麻烦,我们会重点讲那些最关键的技术。
要做一个好的选择器,你必须知道浏览器渲染页面的基本原理、DOM结构、CSS语法,还有浏览器是怎么通过选择器查找元素的。
CSS2选择器在草案中已经解释的非常详细了: Selectors: Pattern Matching 和Appendix G. Grammar of CSS 2.1。
我们把重点放在下面的语法上:
选择器分为以下四种策略:
为了代码的可维护性,我们把这些策略放到一个对象里:
Javascript正则表达式的性能和灵活性足以胜任这样的工作,但是为了让读者更清楚的了解其中的原理,我们将使用一个不同的方法。
大部分编程语言都提供分词的工具。一般词法分析就是建立在它的分词器基础上的。
词法分析器用于进行编程语言的解析,如今我们生活在一个满是计算机数据的世界里。
你会发现,像noko这样的项目(一个Ruby的HTML和XML解析器)已经给了开发者提供了一个词法分析器。
词法分析器的优势在于,它在编程人员和解析器之间提供了一个抽象。使用这些抽象的接口比我们从头去实现容易的多。
让我们选择一个极为简单的词法分析器来取代正则表达式的分词功能。这些规则基于CSS语法说明的规则描述。
我们最好把用到的匹配规则嵌入到一个对象里,避免漏掉哪一个:
要做一个好的选择器,你必须知道浏览器渲染页面的基本原理、DOM结构、CSS语法,还有浏览器是怎么通过选择器查找元素的。
CSS选择器
CSS选择器非常有用,他可以简化复杂结构的选择。解析任何东西都要先了解我们要操作的对象,我会把类库的范围限制在CSS2的一个子集内。CSS2选择器在草案中已经解释的非常详细了: Selectors: Pattern Matching 和Appendix G. Grammar of CSS 2.1。
我们把重点放在下面的语法上:
- E – 匹配所有的标签名称为E的元素
- E F – 匹配所有E元素所属的标签F
- .classname – 匹配所有class属性为classname的所有元素
- E.class – 匹配所有class属性为classname的所有E标签元素
- #id – 匹配所有id值为id的所有元素
- E#id – 匹配所有id值为id的所有E标签元素
以上所有规则被称为简单选择器,简单选择器可以通过空格、">"和"+"等操作符连接。
解析和搜索策略
了解搜索策略最好的方式是通过浏览器。Mozilla开发者网站有一篇文章Writing Efficient CSS 解释了样式的匹配规则。选择器分为以下四种策略:
- ID规则
- 类规则
- 标签规则
- 一般规则
为了代码的可维护性,我们把这些策略放到一个对象里:
findMap = {
'id': function(root, selector) {
},
'name and id': function(root, selector) {
},
'name': function(root, selector) {
},
'class': function(root, selector) {
},
'name and class': function(root, selector) {
}
};
分词器
分词就是把字符分解并归类,这个阶段称为词法分析,这听着好像挺麻烦的。我们拿到一个选择器,我们要做的是:- 删除没用的空白字符
- 把查询字符串解析成我们要查询的指令
- 在DOM上运行这些查询指令
function Token(identity, finder) {
this.identity = identity;
this.finder = finder;
}
Token.prototype.toString = function() {
return 'identity: ' + this.identity + ', finder: ' + this.finder;
};
finder是指我们的选择器类型,就是findMap里定义的那些。identity是选择器的基本规则。
扫描器
Sly和Sizzle都是使用正则表达式实现选择器的,Sizzle把这个称为Chunker。Javascript正则表达式的性能和灵活性足以胜任这样的工作,但是为了让读者更清楚的了解其中的原理,我们将使用一个不同的方法。
大部分编程语言都提供分词的工具。一般词法分析就是建立在它的分词器基础上的。
词法分析器用于进行编程语言的解析,如今我们生活在一个满是计算机数据的世界里。
你会发现,像noko这样的项目(一个Ruby的HTML和XML解析器)已经给了开发者提供了一个词法分析器。
词法分析器的优势在于,它在编程人员和解析器之间提供了一个抽象。使用这些抽象的接口比我们从头去实现容易的多。
让我们选择一个极为简单的词法分析器来取代正则表达式的分词功能。这些规则基于CSS语法说明的规则描述。
我们最好把用到的匹配规则嵌入到一个对象里,避免漏掉哪一个:
macros = {
'nl': '\n|\r\n|\r|\f',
'nonascii': '[^\0-\177]',
'unicode': '\\[0-9A-Fa-f]{1,6}(\r\n|[\s\n\r\t\f])?',
'escape': '#{unicode}|\\[^\n\r\f0-9A-Fa-f]',
'nmchar': '[_A-Za-z0-9-]|#{nonascii}|#{escape}',
'nmstart': '[_A-Za-z]|#{nonascii}|#{escape}',
'ident': '[-@]?(#{nmstart})(#{nmchar})*',
'name': '(#{nmchar})+'
};
rules = {
'id and name': '(#{ident}##{ident})',
'id': '(##{ident})',
'class': '(\\.#{ident})',
'name and class': '(#{ident}\\.#{ident})',
'element': '(#{ident})',
'pseudo class': '(:#{ident})'
};
扫描器的工作方式如下:
- 在macros中展开#{}
- 在展开的macros规则基础上展开#{}
- 编码反斜杠符号
- 把个范式用|连接起来
- 使用RegExp类创建一个全局的正则表达式
使用这些正则表达式
我们得到一个选择器,然后通过扫描和正则表达式把它拆分成几部分。这些工作基于匹配元素的索引:while (match = r.exec(this.selector)) {
finder = null;
if (match[10]) {
finder = 'id';
} else if (match[1]) {
finder = 'name and id';
} else if (match[29]) {
finder = 'name';
} else if (match[15]) {
finder = 'class';
} else if (match[20]) {
finder = 'name and class';
}
this.tokens.push(new Token(match[0], finder));
}
尽管有点罗嗦,但是要比在每个正则表达式里找match[0]要有效的多。
下一篇
我们会在下一篇实现类似FireFox的搜索算法。我们让代码保持简单,并且能通过大部分的浏览器测试如 IE6, IE7, IE8, Firefox, Safari, Chrome 和 Opera。我们要实践基于驱动的开发,来开发我们的解析和分词器。想要看更多的代码,请查看GitHub,turing.dom.js