jQuery-1.9.1源码分析系列(三) Sizzle选择器引擎——编译原理

来源:互联网 发布:矩阵的基是什么 编辑:程序博客网 时间:2024/05/16 14:02
这一节要分析的东东比较复杂,篇幅会比较大,也不知道我描述后能不能让人看明白。这部分的源码我第一次看的时候也比较吃力,现在重头看一遍,再分析一遍,看能否查缺补漏。

  看这一部分的源码需要有一个完整的概念后去看才比较容易看懂,所以我们先把整个编译的原理阐述以后再进行解析。

  还是以上次的那个CSS选择器为例:#chua > a + .group labe[for="age"]。按照我们正常解析CSS的思路从右往左解析(这是效率较高的处理方法),解析之前词法分析完毕,词法分析结果保存在tokens中。

  正常的思路:我们正常情况下的想法是这样的,先取出tokens的最后一个元素(pop出来),然后判断他是选择器的那种情况(这里会有一大堆if/else),然后进入某个分支,将满足条件的DOM节点先取出来存在seed中。然后再次取出最后一个元素,然后又是一大堆判断,进入某个分支,遍历先前的seed剔除掉不满足条件的节点。然后继续先前的处理,直到tokens中的东东没有为止。这是典型的边解析(解析词语:if/else判断)边处理(执行查找)的方式,在解析下一个词语之前,我们是不知道他将要进入那个分支进行处理。

  Sizzle的思路:先准备备选种子【Sizzle最终执行结果是其子集】elems(指定或者document.getElementsByTagName("*"));对每一个tokens的词语,我都有相应的匹配函数来判断某个备选种子是否满足条件【分为两种情况:实体选择器(除开关系选择器外的其他选择器)直接比较种子是否满足实体条件即可;关系选择器(">"/"+"/" "/"~")需要和前一个实体选择器共同组成一个判断函数】,我们将所有的匹配函数连接起来返回一个整体的匹配函数,最后我们将所有的种子一一遍历,代入这个整体匹配函数中执行,返回真表示是我们需要的结果,压入缓存数组中保存起来;返回假直接剔除即可。遍历匹配完所有的种子,我们就得到了想要的结果了。这和我们正常的思路(边解析边处理)完全不一样,属于先解析(解析tokens生成一个整体匹配函数)再处理(遍历所有种子带入整体匹配函数得到匹配结果)。举一个简单的例子:"p > span",假设"span"的匹配函数为match1,"p > "的匹配函数为match2,那么我必须满足匹配条件:allMatch = match1+match2才是我们想要的结果,需要注意的是我们添加最终匹配函数的时候是根据CSS选择器从左到右添加,但是执行最终匹配函数的时候是确实从右到左执行的(match2 -> match1),优点像栈先入后出的赶脚,后续分析生成最终匹配函数的时候我们在详细说明。调用匹配函数的。遍历种子集合seeds,将seeds[0]代入allMatch中执行,返回结果为true则保存起来,返回结果为false则略过;接着讲seeds[1]代入allMatch中执行……

  好了,明白Sizzle的思路以后,离看懂源码就不远了。

 

  先来一点编译前的准备工作。

a. setDocument函数详解


  在tokenize函数开始的时候我们就遇到setDocument函数。该函数功能是设置document,探测浏览器对各个方法属性的支持情况保存在support对象中,并且给Sizzle选择器的匹配关键变量添加匹配相关的hooks,初始化contains和sortOrder函数。

  事关兼容问题,这里也要提一下,把support中主要的属性列一下(IE6/IE7相关的兼容性问题就不管了,现在还有谁为了网吧机搞兼容,费力不讨好)。

support = {  // 判断getElementsByTagName是否只返回节点元素,而不返回空文本节点;true(不返回空文本节点),false(会返回空文本节点)  //比如<span></span>只有一个节点,有的浏览器却返回两个节点:span节点和一个文本节点【空文本】  tagNameNoComments: Boolean,  //判断getAttribute返回值是否正确(IE8对不存在的属性返回为不是null,而是"";其他浏览器都返回null),ture(返回值正确),false(返回值有误)  attributes:Boolean,  
  //
检查getElementsByClassName的值是否可靠(有的浏览器不支持该方法;  //Opera 9.6不能找到第二个class,比如class "hidden e"使用该方法只能找到“hidden”;  //Safari 3.2缓存属性却没有及时跟新,如使用div.className = "e",最后使用getElementsByClassName函数找不到class为“e”的节点)  //true(可靠),false(不可靠)  getByClassName : Boolean,  //检查使用getElementsByName查询的结果是否可靠(有的浏览器ID替换name来查询;)  //true(可靠),false(不可靠)  getByName :Boolean,      //检查使用getElementById查询的结果是否可靠(有的浏览器是用name替换Id来查询)  //true(可靠,ID不会别name查询替换),false(不可靠)  getIdNotName:Boolean,  //是否支持高级查询querySelectorAll,true(支持),false(不支持)  //如果是支持querySelectorAll,将各个浏览器对querySelectorAll接收参数的差异(具体的差异看源码)保存在rbuggyQSA中最后组成正则来判断CSS选择器是否符合要求  qsa: Boolean,  //是否支持浏览器自己的matchesSelector方法(可能带有自己的前缀),true(支持),false(不支持)  //如果是支持matchesSelector,将各个浏览器对matchesSelector接收参数的差异(具体的差异看源码)保存在rbuggyMatches中最后组成正则来判断CSS选择器是否符合要求  matchesSelector: Boolean,      //判断不在dom树上的文档是否可以使用matchesSelector,true(支持),false(不支持)  disconnectedMatch: Boolean,}

  

  初始化查找和匹配相关的hooks。其中 Expr.filter在原子匹配器中详解

// ID find and filterif ( support.getIdNotName ) {    Expr.find["ID"] = function( id, context ) {        if ( typeof context.getElementById !== strundefined && !documentIsXML ) {            var m = context.getElementById( id );            // Check parentNode to catch when Blackberry 4.6 returns            // nodes that are no longer in the document #6963            return m && m.parentNode ? [m] : [];        }    };    Expr.filter["ID"] = function( id ) {       ...    };} else {    Expr.find["ID"] = function( id, context ) {        ...    };    Expr.filter["ID"] =  function( id ) {        ...    };}// TagExpr.find["TAG"] = support.tagNameNoComments ?function( tag, context ) {...} :function( tag, context ) {...};// NameExpr.find["NAME"] = support.getByName && function( tag, context ) {...};// ClassExpr.find["CLASS"] = support.getByClassName && function( className, context ) {...};

  

  初始化Sizzle.contains的内部基础方法contains:有原生方法就使用原生contains或compareDocumentPosition【有兴趣的童鞋可以查一下】来处理,不行则使用节点方法parentNode来比较(比较简单,直接看源码)

  初始化节点排序方法sortOrder(后续再解析)

// Document order sortingsortOrder = docElem.compareDocumentPosition ?function( a, b ) {    ...} :function( a, b ) {    ...};

   

  Sizzle引擎最注重效率,在正真开始编译处理之前,想方设法缩小处理范围,这就是编译函数之前的最后一道处理:select处理

 

b. Sizzle选择器缩小处理范围的函数——select函数详解


  select函数时解析的正真入口,他的作用主要是在针对如果只有一组选择器tokens(选择器没有用逗号分隔)的情况下在编译前缩小查找范围。这里指的缩小范围有两种方式

  第一种,缩小要查找的文档范围(也就是参数中的context)。这种缩小范围只针对tokens(词法解析结果集)中第一个token(词语)为id,并且id的下一个token是关系节点(">"/"+"/" "/"~")的情况,将查找文档范围缩小为id所指定的节点。

//如果第一个token是id且后面紧跟关系选择器,我们可以通多Expr.find["ID"]缩小范围contextif ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&  context.nodeType === 9 && !documentIsXML && Expr.relative[ tokens[1].type ] ) {  
  context
= Expr.find["ID"]( token.matches[0].replace( runescape, funescape ), context )[0];  if ( !context ) {    return results;  }  //去除第一个id token  selector = selector.slice( tokens.shift().value.length );}

 

  第二种,缩小备选种子范围(也就是参数中的seed)。这种情况针对第一个token不是(">"/"+"/"~")的任何一个【第一个token为这三种情况缩小备选种子效率不高?】且整个选择器中包含Sizzle自定义的位置伪类(位置伪类有:first、last、even、odd、eq(n)、gt(n)、lt(n))【自定义位置伪类缩小种子范围的效率比较高?】。取最后一个type为ID/NAME/CLASS/TAG的token【后面没有任何关系选择器,比如"p .chua >"是不满足条件的;"p .chua > span:first-child"取"span"来后去备选种子】查找备选种子seed。

//其中matchExpr["needsContext"]为/^[\x20\t\r\n\f]*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\([\x20\t\r\n\f]*((?:-\d)?\d*)[\x20\t\r\n\f]*\)|)(?=[^-]|$)/i//意味着如果第一个token是[>+~]关系选择器或者整个选择器中有Sizzle自定义的位置伪类选择器则在后面处理,因为此时i为0了i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;//从后向前查找while ( i-- ) {  token = tokens[i];   // 遇到关系选择器则终止循环  if ( Expr.relative[ (type = token.type) ] ) {    break;  }  //处理type为ID/NAME/CLASS/TAG的token  if ( (find = Expr.find[ type ]) ) {    if ( (seed = find(token.matches[0].replace( runescape, funescape ),      rsibling.test( tokens[0].type ) && context.parentNode || context)) ) {      
      //
如果seed为空或者没有tokens,我们就可以更早的返回      tokens.splice( i, 1 );      selector = seed.length && toSelector( tokens );      if ( !selector ) {        push.apply( results, slice.call( seed, 0 ) );        return results;      }      //找到seed,终止循环。
      
break;    }  }}

  最终通过调用compile函数生成终极匹配器执行函数,传入缩小范围后的seed、结构保存数组results等参数,最终匹配的结果保存在了results中。

compile( selector, match )(  seed,  context,  documentIsXML,  results,  rsibling.test( selector ));return results;

  细节请查看源码。

  

c. 原子匹配器 


  何为原子匹配器?也就是我们通常说的最小的匹配器,匹配一个单一的token类型。上一节我们进行词法分析解析出来的结果tokens为

  

  每一个token都有对应的类型(type),每个类型的token都有唯一的原子匹配器。token的类型(ATTR/CHILD/CLASS/ID/PSEUDO/TAG)对应的匹配器如下

  

  比如,对于类型为ID的原子匹配器为

//原子匹配器生成工厂
Expr.filter["ID"] = function( id ) {  var attrId = id.replace( runescape, funescape );
  //生成一个ID原子匹配器
  return function( elem ) {    return elem.getAttribute("id") === attrId;  };};

   我们假设词法分析结果tokens只有一个类型为ID的token。Sizzle根据这个tokens生成了一个ID原子匹配器(这个原子匹配器就是最终匹配器),将某个种子节作为参数传到这个匹配器中得到返回结果:true表示这个种子节点匹配这个最总匹配器,false表示该种子节点不匹配这个种子节点,一目了然。

   其他的原子匹配器这里不再分析,各位童鞋可以逐字逐句取看。后期如果有空的话,我在去分析一下子选择器的原子匹配器和伪类选择器的原子匹配器。我们在这里现有原子匹配器这个概念。

  在这里大家可能就有点疑问了:关系选择器(">"/"+"/" "/"~")没有原子匹配器,怎么处理?这就是编译处理compile该处理的了,将关系选择器附加到前一个(左边一个token)上。举个例子“p > .chua”。p和.chua都有自己的原子匹配器,关系选择器“>”的处理也比较简单,他和p结合起来,只要找到节点的父节(parentNode)点能匹配上p的原子匹配器即可,详情在后面解析。

 

  Sizzle引入了编译概念,这里所谓的编译就是根据词法分析结果tokens生成最终匹配器的过程。

d. 编译函数compile详解


  compile函数目的是生成一个终极匹配器执行函数。

  compile中最主要的是两个函数:

  matcherFromTokens函数为一组【“p > .chua , span”中逗号将CSS选择器分成两组】tokens生成一个终极匹配器函数,被压入到setMatchers或者elementMatchers中。

  matcherFromGroupMatchers函数生成一个函数superMatcher返回,superMatcher函数目的是将备选DOM节点一一和终极匹配器匹配,成功则把该DOM节点存入results中。

  

  compile 源码如下:

compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) {  var i,    setMatchers = [],    elementMatchers = [],    //查询当前选择器是否有被处理并保存在缓存compilerCache中,    //如果有,直接返回即可,jQuery的缓存无处不在    cached = compilerCache[ selector + " " ];    
  if
( !cached ) {    //如果没有被词法解析先进行词法解析    if ( !group ) {      group = tokenize( selector );    }    i = group.length;    while ( i-- ) {      //生成匹配器函数      cached = matcherFromTokens( group[i] );      //如果选择器中有伪类的选择器压入setMatchers,cached[expando]在生成匹配器函数的时候就判断是否有伪类而赋值了      if ( cached[ expando ] ) {        setMatchers.push( cached );      //普通选择器压入elementMatchers      } else {        elementMatchers.push( cached );      }    }    // compilerCache缓存编译函数    //matcherFromGroupMatchers函数返回一个执行所有的匹配器最终将匹配成功的集合保存在作为参数传递过来的数组对象results中的curry化函数    cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );  }  return cached;};

  创建伪类标志的函数

function markFunction( fn ) {       //创建伪类标志       fn[ expando ] = true;       return fn;}

 

  其中最为重要的函数matcherFromTokens:一组tokens最终匹配器的生成函数来袭。

e. 一组匹配器(tokens)最终匹配器的生成函数matcherFromTokens及相关


   在详解matcherFromTokens函数之前我们先说说生成终极匹配器的原理。

  首先我们知道除了关系选择器“ ”、“+”、“>”,“~”之外的原子选择器(包括伪类,这里原子选择器指单个选择器如“#mm”、“.te”、“input”)都有对应的原子匹配器可以判断节点是否匹配。比如对于TAG类型的选择器的原子匹配函数为:

"TAG": function( nodeName ) {  if ( nodeName === "*" ) {
    //生成原子匹配器
    return function() { return true; };  }  nodeName = nodeName.replace( runescape, funescape ).toLowerCase();
  //生成原子匹配器
  return function( elem ) {    return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;  };  }

  但是对于关系选择器则不同,他们没有原子匹配器,他们有的只是匹配关系。比如“div > p”。这个该如何处理呢。Sizzle巧妙的使用了addCombinator函数。addCombinator函数将“>”的处理和“div”关联起来。

  首先需要明白Sizzle添加最终匹配函数的时候是根据CSS选择器从左到右添加,但是执行最终匹配函数的时候是确实从右到左执行的,有点像栈先入后出的赶脚。接下来我们用一个比较简单的例子来模拟最终匹配函数的生成过程。

  我们逐一分解“div > p”成匹配函数组。

  首先,我们知道matchers用来保存匹配函数集合。遇到div,生成元匹配函数pDiv,插入matchers数组中变成了[基础匹配函数,pDiv]

  然后遇到“>”,调用addCombinator将“>”关系函数附加到先前的匹配中matchers数组变成了[“>”关系匹配:[基础匹配函数,pDiv]]。我们将“>”的addCombinator的工作代码放在这里:【可以看到关系选择器只是在查找关系节点而已,并未做任何原子匹配。这里dir: "parentNode";matcher:执行匹配器函数elementMatcher( [基础匹配函数,pDiv])。这里明了了吧,一旦执行到关系匹配器“>”,函数就去查找节点的直接父节点,然后在执行父节点那一级该执行的匹配函数match】

//如果是紧密关系>或+选择器function( elem, context, xml ) {  //循环获取指定关系节点,紧密关系一般一次就进入if语句  while ( (elem = elem[ dir ]) ) {    //如果elem是节点或选择器关系是父子关系则返回匹配函数    if ( elem.nodeType === 1 || checkNonElements ) {      return matcher( elem, context, xml );    }  }}

  最后遇到“p”,生成元匹配函数pP压入matchers中变成[“>”关系匹配:[基础匹配函数,pDiv],pP]。到此,匹配函数已经生成完毕。

  最终匹配函数生成完成以后,我们执行这个匹配函数是从右往左执行的,所以先执行pP,然后执行关系匹配函数(“>”关系匹配:[基础匹配函数,pDiv]),然后执行pDiv,最后执行默认的匹配函数(基础匹配函数)。  

  接下来假设有这么一个elem节点。elem去匹配matchers是从数组最后一个开始匹配的,符合选择器从右到左的策略。如果elem代入pP返回true,然后代入“>”关系匹配:[基础匹配函数,pDiv]中(根据从右到左的策略就是匹配亲生父节点调用匹配函数pDiv),如果成功则继续匹配基础匹配函数(这个函数一般不会有问题,返回true),否则返回false。到此节点elem的匹配结果就已经出来了。

  看一看最终生成的匹配器结构,可以看出一大堆matchers和matcher的嵌套

 

      

  matcherFromTokens的源码如下:
function matcherFromTokens( tokens ) {  var checkContext, matcher, j,    len = tokens.length,    leadingRelative = Expr.relative[ tokens[0].type ],    implicitRelative = leadingRelative || Expr.relative[" "],    i = leadingRelative ? 1 : 0,    // 基础匹配,确保元素从最顶层的context可达    matchContext = addCombinator( function( elem ) {      return elem === checkContext;    }, implicitRelative, true ),       matchAnyContext = addCombinator( function( elem ) {      return indexOf.call( checkContext, elem ) > -1;    }, implicitRelative, true ),       //这里用来确定元素在哪个context,也是最后一个执行的匹配器    matchers = [ function( elem, context, xml ) {      return ( !leadingRelative && ( xml || context !== outermostContext ) ) || (        (checkContext = context).nodeType ?        matchContext( elem, context, xml ) :        matchAnyContext( elem, context, xml ) );    } ];  for ( ; i < len; i++ ) {    //如果有父子选择器(关系选择器)    if ( (matcher = Expr.relative[ tokens[i].type ]) ) {      //整合匹配器组      matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];    //生成元匹配器    } else {      matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );              //返回一个特殊的位置匹配函数      //伪类会把selector分两部分      if ( matcher[ expando ] ) {        //发现下一个关系操作符(如果有的话)并做相应处理        j = ++i;        for ( ; j < len; j++ ) {          if ( Expr.relative[ tokens[j].type ] ) {            break;          }        }        return setMatcher(          i > 1 && elementMatcher( matchers ),          i > 1 && toSelector( tokens.slice( 0, i - 1 ) ).replace( rtrim, "$1" ),          matcher,          i < j && matcherFromTokens( tokens.slice( i, j ) ),//如果伪类和其后最近的关系选择器之间还有选择器要筛选          j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),//如果伪类其后最近的关系选择器后面还有选择器要筛选          j < len && toSelector( tokens )        );      }      matchers.push( matcher );    }  }  return elementMatcher( matchers );}

  

  addCombinator将关系选择器处理包裹住前一个原子选择器的匹配函数上

//整合关系选择器,将关系选择器附加到matcher上//matcher主要用来判断elem是否符合要求function addCombinator( matcher, combinator, base ) {  var dir = combinator.dir,    checkNonElements = base && dir === "parentNode",    doneName = done++;   
  return
combinator.first ?    //如果是紧密关系>或+选择器    function( elem, context, xml ) {      //循环获取指定关系节点,紧密关系一般一次就进入if语句      while ( (elem = elem[ dir ]) ) {        //如果elem是节点或选择器关系是父子关系则返回匹配函数        if ( elem.nodeType === 1 || checkNonElements ) {          return matcher( elem, context, xml );        }      }    } :    // Check against all ancestor/preceding elements    function( elem, context, xml ) {      var data, cache, outerCache,        dirkey = dirruns + " " + doneName;      //我们不能在XML节点上设置任何数据,所以他们不能从dir缓存中受益      if ( xml ) {        while ( (elem = elem[ dir ]) ) {          if ( elem.nodeType === 1 || checkNonElements ) {            if ( matcher( elem, context, xml ) ) {              return true;            }          }        }      } else {        //循环获取指定关系节点        while ( (elem = elem[ dir ]) ) {          if ( elem.nodeType === 1 || checkNonElements ) {            outerCache = elem[ expando ] || (elem[ expando ] = {});            //outerCache有当前关系数据数组,且数组第一位和dirkey相等            if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) {              if ( (data = cache[1]) === true || data === cachedruns ) {                return data === true;              } } else { cache = outerCache[ dir ] = [ dirkey ]; cache[1] = matcher( elem, context, xml ) || cachedruns; if ( cache[1] === true ) { return true; }            }          }        }      }    };}

  返回一个执行多个选择器匹配的匹配函数(匹配器)

function elementMatcher( matchers ) {  return matchers.length > 1 ?    //如果是多个匹配器的情况,那么就需要elem符合全部匹配器规则       function( elem, context, xml ) {      var i = matchers.length;          //从后往前匹配          while ( i-- ) {        if ( !matchers[i]( elem, context, xml ) ) {                return false;              }          }          return true;    } :       //单个匹配器的话就返回自己即可       matchers[0];}

  setMatcher伪类分隔器这个放在后一节解析。

  

  如果觉得本文不错,请点击右下方【推荐】!

0 0
原创粉丝点击