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

欢迎来我的专栏检察系列文章。

在编译道理中,词法剖析是一个异常症结的环节,词法剖析器读入字撙节,然后依据症结字、标识符、标点、字符串等举行分别,天生单词。Sizzle 挑选器的婚配思绪和这异常像,在内部叫做 Tokens。

《jQuery 源码系列(四)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 > + ~

  1. div,p , 示意并列关联,一切 div 元素和 p 元素;

  2. div p 空格示意子女元素,div 元素内一切的 p 元素;

  3. div>p > 子元素且相差只能是一代,父元素为 div 的一切 p 元素;

  4. div+p + 示意紧邻的兄弟元素,前一个兄弟节点为 div 的一切 p 元素;

  5. div~p ~ 示意兄弟元素,一切前面有兄弟元素 div 的一切 p 元素。

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

  1. a[target=_blank] 挑选一切 target 为 _blank 的一切 a 元素;

  2. a

    挑选一切 title 为 search 的一切 a 元素;

  3. input[type=text] 挑选 type 为 text 的一切 input 元素;

  4. 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\t\r\n\f]*,[\x20\t\r\n\f]*/;
rcomma.exec('div > div.seq h2 ~ p');//null
rcomma.exec(' ,#id p');//[" ,"]

rcomma 这个正则,主如果用来辨别 selector 是不是到下一个划定规矩,假如到下一个划定规矩,就把之前处置惩罚好的 push 到 groups 中。这个正则中 [\x20\t\r\n\f] 是用来婚配类似于 whitespace 的,主体就一个逗号。

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

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

实在,假如看 jQuery 的源码,rcomma 和 rcombinators 并非如许来定义的,而是用下面的体式格局来定义:

var whitespace = "[\\x20\\t\\r\\n\\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-]|[^\0-\\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。

欢迎来我的博客交换。

    原文作者:songjz
    原文地址: https://segmentfault.com/a/1190000008365751
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞