js框架开发之旅--选择器二

这一篇我要演示如何实现一个选择器引擎。实现一个选择器比想象中的要麻烦,我们会重点讲那些最关键的技术。
要做一个好的选择器,你必须知道浏览器渲染页面的基本原理、DOM结构、CSS语法,还有浏览器是怎么通过选择器查找元素的。


CSS选择器

CSS选择器非常有用,他可以简化复杂结构的选择。解析任何东西都要先了解我们要操作的对象,我会把类库的范围限制在CSS2的一个子集内。
CSS2选择器在草案中已经解释的非常详细了: Selectors: Pattern MatchingAppendix 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规则
  •     类规则
  •     标签规则
  •     一般规则
选择器的最后一部分(最右边部分)称为关键选择器。浏览器首先通过关键选择器过滤出所有符合规则的元素集合,然后再通过其他规则进行过滤(从右到左)。因此我们查询element#idName的查询速度要比#idName慢。这种匹配方式并不是最快的,但这却是最容易理解和最有效的方式。
为了代码的可维护性,我们把这些策略放到一个对象里:
  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类创建一个全局的正则表达式
这个过程会产生大量的正则表达式,这和Sizzle及Sly使用正则表达式是类似的。这样的好处是你可以清楚的看清选择器和DOM匹配搜索之间的关系。


使用这些正则表达式

我们得到一个选择器,然后通过扫描和正则表达式把它拆分成几部分。这些工作基于匹配元素的索引:
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


牧客网--让自由职业成为一个靠谱的工作


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值