资讯专栏INFORMATION COLUMN

jQuery 源码系列(四)Tokens 词法分析

rollback / 3086人阅读

摘要:欢迎来我的专栏查看系列文章。我们以为例,这是一个很简单的,逗号将表达式分成两部分。这是针对于存在的情况,对于不存在的情况,其就是的操作,后面会谈到。参考源码分析引擎词法解析选择器参考手册本文在上的源码地址,欢迎来。

欢迎来我的专栏查看系列文章。

在编译原理中,词法分析是一个非常关键的环节,词法分析器读入字节流,然后根据关键字、标识符、标点、字符串等进行划分,生成单词。Sizzle 选择器的匹配思路和这非常像,在内部叫做 Tokens。

Tokens 词法分析

其实词法分析是汇编里面提到的词汇,把它用到这里感觉略有不合适,但 Sizzle 中的 tokensize函数干的就是词法分析的活。

上一章我们已经讲到了 Sizzle 的用法,实际上就是 jQuery.find 函数,只不过还涉及到 jQuery.fn.find。jQuery.find 函数考虑的很周到,对于处理 #id、.class 和 TagName 的情况,都比较简单,通过一个正则表达式 rquickExpr 将内容给分开,如果浏览器支持 querySelectorAll,那更是最好的。

比较难的要数这种类似于 css 选择器的 selector,div > div.seq h2 ~ p , #id p,如果使用从左向右的查找规则,效率很低,而从右向左,可以提高效率。

本章就来介绍 tokensize 函数,看看它是如何将复杂的 selector 处理成 tokens 的。

我们以 div > div.seq h2 ~ p , #id p 为例,这是一个很简单的 css,逗号 , 将表达式分成两部分。css 中有一些基本的符号,这里有必要强调一下,比如 , space > + ~

div,p , 表示并列关系,所有 div 元素和 p 元素;

div p 空格表示后代元素,div 元素内所有的 p 元素;

div>p > 子元素且相差只能是一代,父元素为 div 的所有 p 元素;

div+p + 表示紧邻的兄弟元素,前一个兄弟节点为 div 的所有 p 元素;

div~p ~ 表示兄弟元素,所有前面有兄弟元素 div 的所有 p 元素。

除此之外,还有一些 a、input 比较特殊的:

a[target=_blank] 选择所有 target 为 _blank 的所有 a 元素;

a[title=search] 选择所有 title 为 search 的所有 a 元素;

input[type=text] 选择 type 为 text 的所有 input 元素;

p:nth-child(2) 选择其为父元素第二个元素的所有 p 元素;

Sizzle 都是支持这些语法的,如果我们把这一步叫做词法分析,那么词法分析的结果是一个什么东西呢?

div > div.seq h2 ~ p , #id p 经过 tokensize(selector) 会返回一个数组,该数组在函数中称为 groups。因为这个例子有一个逗号,故该数组有两个元素,分别是 tokens[0] 和 tokens[1],代表选择器逗号前后的两部分。tokens 也是数组,它的每一个元素都是一个 token 对象。

一个 token 对象结构如下所示:

token: {
  value: matched, // 匹配到的字符串
  type: type, //token 类型
  matches: match //去除 value 的正则结果数组
}

Sizzle 中 type 的种类有下面几种:ID、CLASS、TAG、ATTR、PSEUDO、CHILD、bool、needsContext,这几种有几种我也不知道啥意思,child 表示 nth-child、even、odd 这种子选择器。这是针对于 matches 存在的情况,对于 matches 不存在的情况,其 type 就是 value 的 trim() 操作,后面会谈到。

tokensize 函数对 selector 的处理,连空格都不放过,因为空格也属于 type 的一种,而且还很重要,div > div.seq h2 ~ p 的处理结果:

tokens: [
  [value:"div", type:"TAG", matches:Array[1]],
  [value:" > ", type:">"],
  [value:"div", type:"TAG", matches:Array[1]],
  [value:".seq", type:"CLASS", matches:Array[1]],
  [value:" ", type:" "],
  [value:"h2", type:"TAG", matches:Array[1]],
  [value:" ~ ", type:"~"],
  [value:"p", type:"TAG", matches:Array[1]],
]

这个数组会交给 Sizzle 的下一个流程来处理,今天暂不讨论。

tokensize 源码

照旧,先来看一下几个正则表达式。

var rcomma = /^[x20	
f]*,[x20	
f]*/;
rcomma.exec("div > div.seq h2 ~ p");//null
rcomma.exec(" ,#id p");//[" ,"]

rcomma 这个正则,主要是用来区分 selector 是否到下一个规则,如果到下一个规则,就把之前处理好的 push 到 groups 中。这个正则中 [x20 f] 是用来匹配类似于 whitespace 的,主体就一个逗号。

var rcombinators = /^[x20	
f]*([>+~]|[x20	
f])[x20	
f]*/;
rcombinators.exec(" > div.seq h2 ~ p"); //[" > ", ">"]
rcombinators.exec(" ~ p"); //[" ~ ", "~"]
rcombinators.exec(" h2 ~ p"); //[" ", " "]

是不是看来 rcombinators 这个正则表达式,上面 tokens 那个数组的内容就完全可以看得懂了。

其实,如果看 jQuery 的源码,rcomma 和 rcombinators 并不是这样来定义的,而是用下面的方式来定义:

var whitespace = "[x20	
f]";
var rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
  rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ),
  rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^])(?:.)*)" + whitespace + "+$", "g" ),

有的时候必须得要佩服 jQuery 中的做法,该简则简,该省则省,每一处代码都是极完美的。

还有两个对象,Expr 和 matchExpr,Expr 是一个非常关键的对象,它涵盖了几乎所有的可能的参数,比较重要的参数比如有:

Expr.filter = {
  "TAG": function(){...},
  "CLASS": function(){...},
  "ATTR": function(){...},
  "CHILD": function(){...},
  "ID": function(){...},
  "PSEUDO": function(){...}
}
Expr.preFilter = {
  "ATTR": function(){...},
  "CHILD": function(){...},
  "PSEUDO": function(){...}
}

这个 filter 和 preFilter 是处理 type=TAG 的关键步骤,包括一些类似于 input[type=text] 也是这几个函数处理,也比较复杂,我本人是看迷糊了。还有 matchExpr 正则表达式:

var identifier = "(?:.|[w-]|[^-xa0])+",
    attributes = "[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
    // Operator (capture 2)
    "*([*^$|!~]?=)" + whitespace +
    // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]"
    "*(?:"((?:.|[^"])*)"|"((?:.|[^"])*)"|(" + identifier + "))|)" + whitespace +
    "*]",
    pseudos = ":(" + identifier + ")(?:((" +
    // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:
    // 1. quoted (capture 3; capture 4 or capture 5)
    "("((?:.|[^"])*)"|"((?:.|[^"])*)")|" +
    // 2. simple (capture 6)
    "((?:.|[^()[]]|" + attributes + ")*)|" +
    // 3. anything else (capture 2)
    ".*" +
    "))|)",
    booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped";
var matchExpr = {
  "ID": new RegExp( "^#(" + identifier + ")" ),
  "CLASS": new RegExp( "^.(" + identifier + ")" ),
  "TAG": new RegExp( "^(" + identifier + "|[*])" ),
  "ATTR": new RegExp( "^" + attributes ),
  "PSEUDO": new RegExp( "^" + pseudos ),
  "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" ),
  // For use in libraries implementing .is()
  // We use this for POS matching in `select`
  "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:(" +
    whitespace + "*((?:-d)?d*)" + whitespace + "*)|)(?=[^-]|$)", "i" )
}

matchExpr 作为正则表达式对象,其 key 的每一项都是一个 type 类型,将 type 匹配到,交给后续函数处理。

tokensize 源码如下:

var tokensize = function (selector, parseOnly) {
  var matched, match, tokens, type, soFar, groups, preFilters, cached = tokenCache[selector + " "];
  // tokenCache 表示 token 缓冲,保持已经处理过的 token
  if (cached) {
    return parseOnly ? 0 : cached.slice(0);
  }

  soFar = selector;
  groups = [];
  preFilters = Expr.preFilter;

  while (soFar) {

    // 判断一个分组是否结束
    if (!matched || (match = rcomma.exec(soFar))) {
      if (match) {
        // 从字符串中删除匹配到的 match
        soFar = soFar.slice(match[0].length) || soFar;
      }
      groups.push((tokens = []));
    }

    matched = false;

    // 连接符 rcombinators
    if ((match = rcombinators.exec(soFar))) {
      matched = match.shift();
      tokens.push({
        value: matched,
        type: match[0].replace(rtrim, " ")
      });
      soFar = soFar.slice(matched.length);
    }

    // 过滤,Expr.filter 和 matchExpr 都已经介绍过了
    for (type in Expr.filter) {
      if ((match = matchExpr[type].exec(soFar)) && (!preFilters[type] || (match = preFilters[type](match)))) {
        matched = match.shift();
        // 此时的 match 实际上是 shift() 后的剩余数组
        tokens.push({
          value: matched,
          type: type,
          matches: match
        });
        soFar = soFar.slice(matched.length);
      }
    }

    if (!matched) {
      break;
    }
  }

  // parseOnly 这个参数应该以后会用到
  return parseOnly ? 
    soFar.length : 
    soFar ? 
      Sizzle.error(selector) :
      // 存入缓存
      tokenCache(selector, groups).slice(0);
}

不仅数组,字符串也有 slice 操作,而且看源码的话,jQuery 中对字符串的截取,使用的都是 slice 方法。而且本代码中出现的 array.slice(0)方法是一个浅拷贝数组的好方法。

如果此时 parseOnly 不成立,则返回结果需从 tokenCache 这个函数中来查找:

var tokenCache = createCache();
function createCache() {
  var keys = [];

  function cache( key, value ) {
    // Expr.cacheLength = 50
    if ( keys.push( key + " " ) > Expr.cacheLength ) {
      // 删,最不经常使用
      delete cache[ keys.shift() ];
    }
    // 整个结果返回的是 value
    return (cache[ key + " " ] = value);
  }
  return cache;
}

可知,返回的结果是 groups,tokensize 就学完了,下章将介绍 tokensize 的后续。

总结

对于一个复杂的 selector,其 tokensize 的过程远比今天介绍的要复杂,今天的例子有点简单(其实也比较复杂了),后面的内容更精彩。

参考

jQuery 2.0.3 源码分析Sizzle引擎 - 词法解析
CSS 选择器参考手册

本文在 github 上的源码地址,欢迎来 star。

欢迎来我的博客交流。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/88134.html

相关文章

  • jQuery中的选择器引擎Sizzle

    摘要:生成终极匹配器主要是返回一个匿名函数,在这个函数中,利用方法生成的匹配器,去验证种子集合,筛选出符合条件的集合。在这个终极匹配器中,会将获取到的种子元素集合与匹配器进行比对,筛选出符合条件的元素。 读Sizzle的源码,分析的Sizzle版本号是2.3.3。 Sizzle的Github主页 浏览器原生支持的元素查询方法: 方法名 方法描述 兼容性描述 getElementBy...

    elisa.yang 评论0 收藏0
  • 编译原理实战入门:用 JavaScript 写一个简单的则运算编译器(一)词法分析

    摘要:一般的程序,是无法直接执行的,因为只能识别机器指令。所以要想执行一个程序,首先要将高级语言编写的程序翻译为汇编代码,再将汇编代码翻译为机器指令,这样才能识别并执行。 编译器 编译器是一个程序,作用是将一门语言翻译成另一门语言。 一般的程序,CPU 是无法直接执行的,因为 CPU 只能识别机器指令。所以要想执行一个程序,首先要将高级语言编写的程序翻译为汇编代码,再将汇编代码翻译为机器指令...

    wangdai 评论0 收藏0
  • jQuery 源码系列(六)sizzle 编译

    摘要:一种比较合理的方法就是对应每个可判断的生成一个闭包函数,统一进行查找。根据关系编译闭包函数,为四组编译函数主要借助和。第四步将所有的编译闭包函数放到一起,生成函数。 欢迎来我的专栏查看系列文章。 compile 讲了这么久的 Sizzle,总感觉差了那么一口气,对于一个 selector,我们把它生成 tokens,进行优化,优化的步骤包括去头和生成 seed 集合。对于这些种子集合,...

    Terry_Tai 评论0 收藏0
  • 精读《syntax-parser 源码

    摘要:引言是一个版语法解析器生成器,具有分词语法树解析的能力。实现函数用链表设计函数是最佳的选择,我们要模拟调用栈了。但光标所在的位置是期望输入点,这个输入点也应该参与语法树的生成,而错误提示不包含光标,所以我们要执行两次。 1. 引言 syntax-parser 是一个 JS 版语法解析器生成器,具有分词、语法树解析的能力。 通过两个例子介绍它的功能。 第一个例子是创建一个词法解析器 my...

    yuanxin 评论0 收藏0
  • 构建一个使用 Virtual-DOM 的前端模版引擎

    摘要:目录前言问题的提出模板引擎和结合的实现编译原理相关模版引擎的词法分析语法分析与抽象语法树代码生成完整的结语前言本文尝试构建一个前端模板引擎,并且把这个引擎和进行结合。于是就构思了一个方案,在前端模板引擎上做手脚。 作者:戴嘉华 转载请注明出处并保留原文链接( https://github.com/livoras/blog/issues/14 )和作者信息。 目录 前言 问题的提出...

    imccl 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<