好了有了之前的词法分析过程,现在我们来到select函数来,这个函数的整体流程,前面也大概说过:

1. 先做词法分析获得token列表

2. 如果有种子集合直接到编译过程

3. 如果没有种子集合并且是单组选择符(没有逗号)

(1)尝试缩小上下文:如果第一个token是ID选择符,则会执行Expr.find["ID"]的方法来找到这个上下文,以后所有的查询都是在这个上下文进行,然后把第一个ID选择符剔除。

(2)尝试寻找种子集合:从右开始往左分析token,如果遇到关系选择符(> + ~ 空)终止循环,否则通过Expr.find的方法尝试寻找符合条件的DOM集合,如果找到了就讲种子集合保存起来。

4. 进入到编译过程

这里面需要讲解下为何要进行筛选的工作,前面也说过,目的就是为了尽量缩小查询范围,首先缩小上下文范围,然后缩小种子集合范围,因为从右向左查询的过程更快,所以我们是从后面开始搜索种子集合,搜索到之后,后面所有的分析过程都是在这些种子集合基础之上进行的。

Expr.find = {
  'ID' : context.getElementById,
  'CLASS' : context.getElementsByClassName,
  'NAME' : context.getElementsByName,
  'TAG' : context.getElementsByTagName
}

Expr.find返回一个函数,这个函数根据当前参数进行验证,看看是否是指定类型的节点,可以查验ID,CLASS,NAME和TAG等等。我们以class为例:

  1. Expr.find["CLASS"] = support.getElementsByClassName && function(className, context) {
  2. if (typeof context.getElementsByClassName !== strundefined && documentIsHTML) {
  3. return context.getElementsByClassName(className);
  4. }
  5. };
  1.  

Expr.find["CLASS"]返回一个函数,这个函数有两个参数,第一个参数className,第二个参数context,在select里面就是通过这个函数来查询指定className的DOM集合,找到以后就是seed种子集合。

select源码如下:

  1. function select(selector, context, results, seed) {
  2. var i, tokens, token, type, find,
  3. //解析出词法格式
  4. match = tokenize(selector);
  5.  
  6. if (!seed) { //没有指定seed
  7. // Try to minimize operations if there is only one group
  8. // 单组选择符并且以ID开头,可以做些优化
  9. if (match.length === 1) {
  10.  
  11. // Take a shortcut and set the context if the root selector is an ID
  12. tokens = match[0] = match[0].slice(0); //取出选择器Token序列
  13.  
  14. //找到context上下文
  15. if (tokens.length > 2 && (token = tokens[0]).type === "ID" &&
  16. support.getById && context.nodeType === 9 && documentIsHTML &&
  17. Expr.relative[tokens[1].type]) {
  18.  
  19. context = (Expr.find["ID"](token.matches[0].replace(runescape, funescape), context) || [])[0];
  20. if (!context) {
  21. return results;
  22. }
  23. //去掉以查询的选择符
  24. selector = selector.slice(tokens.shift().value.length);
  25. }
  26.  
  27. // Fetch a seed set for right-to-left matching
  28. //伪类方面,暂不讨论
  29. i = matchExpr["needsContext"].test(selector) ? 0 : tokens.length;
  30.  
  31. //从右向左边查询
  32. while (i--) {
  33. token = tokens[i]; //找到后边的规则
  34.  
  35. // Abort if we hit a combinator
  36. // 遇到关系选择符终止 if (Expr.relative[(type = token.type)]) {
  37. break;
  38. }
  39. //对选择符进行过滤查询,看看能否找到符合条件的节点
  40. if ((find = Expr.find[type])) {
  41.  
  42. // Search, expanding context for leading sibling combinators
  43. if ((seed = find(
  44. token.matches[0].replace(runescape, funescape),
  45. rsibling.test(tokens[0].type) && context.parentNode || context
  46. ))) {
  47. // If seed is empty or no tokens remain, we can return early
  48. tokens.splice(i, 1);
  49. selector = seed.length && toSelector(tokens);
  50.  
  51. if (!selector) {//如果选择符为空,查询完毕
  52. push.apply(results, seed);
  53. return results;
  54. }
  55.  
  56. break;
  57. }
  58. }
  59. }
  60. }
  61. } compile(selector, match)(
  62. seed,
  63. context, !documentIsHTML,
  64. results,
  65. rsibling.test(selector)
  66. );
  67. return results;
  68. }

走到这里我们发现,我们现在已经拥有了哪些信息:token列表,缩小的context和种子集合,那么剩下的事情是不是对种子集合的每个元素再和token列表一一校验,留下符合条件的,删除不符合条件的是不是查询就完成了?
正常看起来是这样的,我们对每个种子进行边解析边分析的过程符合要求,但是Sizzle做了更进一步的处理,通过空间换时间的方式,提高了查询性能,他采用了一种叫先编译后执行的过程。首先把所有的token元素生成一个嵌套的函数,然后再针对种子集合,去执行这个函数,把符合条件的留下来,由于函数是通过闭包的方式来保存,所以当同一个选择符查询时,可以直接执行函数来查询,从而加快了查询的性能,而不用每次从头解析。

这个函数包括两种情况:

1. 关系选择器:如果token是关系选择器,则生成函数的时候需要同上一个选择器共同生成。

2. 非关系选择器:如果是非关系选择器,则直接判断种子是否满足条件即可。

比如 div > a 我们生成函数1 父节点是否是div 函数2 本身是否是a标签 函数1+函数2 就是我们最终生成的Match匹配函数,对每个种子进行执行Match匹配函数即可。

我们看看compile的整体思路

1. 从缓存查询是否已经编译过,有的话直接拿出来

2. 判断是否tokenize过,没有的话,补一下

3. 对group的每个元素进行matcherFromTokens方法,获得该token组的组合函数,如果是包含伪类,则添加到setMatchers数组,否则添加到elementMatchers数组

4. 最后对setMatchers和elementMatchers执行matcherFromGroupMatchers方法。

这里要解释下matcherFromTokens和matcherFromGroupMatchers方法,生成最终的包含非伪类和伪类的最终匹配函数:

matcherFromTokens: 将一组token数组转换为一个Match匹配函数,比如div > a 就生成一个包含两个函数的Match匹配函数。

matcherFromGroupMatchers:由于存在伪类和非伪类选择符两种情况,这个函数的目的是融合这两种情况,最终生成一个超级匹配函数。

  1. compile = Sizzle.compile = function(selector, group /* Internal Use Only */ ) {
  2. var i,
  3. setMatchers = [],
  4. elementMatchers = [],
  5. cached = compilerCache[selector + " "];
  6.  
  7. if (!cached) {//看看是否有缓存
  8. // Generate a function of recursive functions that can be used to check each element
  9. if (!group) {
  10. group = tokenize(selector);
  11. }
  12.  
  13. i = group.length;
  14.  
  15. //对每个分组进行遍历
  16. while (i--) {
  17. //获得Match匹配函数
  18. cached = matcherFromTokens(group[i]);
  19. if (cached[expando]) {//包含伪类的添加到setMatchers数组
  20. setMatchers.push(cached);
  21. } else { //否则添加到elementMatchers数组
  22. elementMatchers.push(cached);
  23. }
  24. }
  25.  
  26. // Cache the compiled function
  27. // 最终融合成一个超级匹配函数返回
  28. cached = compilerCache(selector, matcherFromGroupMatchers(elementMatchers, setMatchers));
  29. }
  30. //
  31. return cached;
  32. };

下面介绍下matcherFromTokens这个方法:输入参数是tokens,然后对每个token进行处理,这里需要了解一个知识点:

非伪类的选择符 有普通选择符和关系选择符两种,关系选择符包括以下几种情况:> 空格 + ~

保存在Expr.relative对象中

> : 表示是父子关系 对应DOM属性parentNode 是元素的第一个节点所以 first为true

空格:表示是后代关系 对应DOM属性parentNode

+:表示附近兄弟关系 对应DOM属性previousSibling 是元素的第一个节点所以 first为true

~:表示普通兄弟关系 对应DOM属性previousSibling

在matcherFromTokens方法中就会对非关系型和关系型分部处理:

matchers是存放各个选择符过滤函数的数组

1. 非关系型运算符:把该类型的过滤函数拷贝一份push到matchers数组中即可,比如前面#div_test > span input[checked=true]中的 input span等等

2. 关系型运算符:把当前的关系选择符和前面的选择符一起共同组成一个过滤函数,push到matchers数组中。

最后把matchers数组统一通过elementMatcher函数来生成一个最终的过滤函数

elementMatcher方法的作用是将一个函数数组,生成一个过滤函数,这个函数会遍历执行各个函数

  1. //将mathcers数组的所有方法合并成一个单独的函数,这个函数会挨个执行数组中的方法。
  2. function elementMatcher(matchers) {
  3. return matchers.length > 1 ?
  4. //如果是多个匹配器,循环判断
  5. function(elem, context, xml) {
  6. var i = matchers.length;
  7. //从右到左开始匹配
  8. while (i--) {
  9. //如果有一个没匹配中,返回false
  10. if (!matchers[i](elem, context, xml)) {
  11. return false;
  12. }
  13. }
  14. return true;
  15. } :
  16. //单个匹配器的话就返回自己即可
  17. matchers[0];
  18. }
  19.        

addCombinator为关系选择符生成过滤函数,将上一个选择符和关系选择符联合起来查询

  1. //关系选择器过滤函数生成器,根据关系选择符的类型,返回一个关系选择符过滤函数
  2. function addCombinator(matcher, combinator, base) {
  3. var dir = combinator.dir,
  4. checkNonElements = base && dir === "parentNode",
  5. doneName = done++;
  6.  
  7. return combinator.first ?
  8. // Check against closest ancestor/preceding element、
  9.      //如果first为true表示是 > 或者 + 选择符 取最近的一个元素即可,一次查询
  10. function(elem, context, xml) {
  11. while ((elem = elem[dir])) {
  12. if (elem.nodeType === 1 || checkNonElements) {
  13. //找到第一个节点,直接执行过滤函数即可
  14. return matcher(elem, context, xml);
  15. }
  16. }
  17. } :
  18.  
  19. // Check against all ancestor/preceding elements
  20. //否则就是空格或~需要查询所有父节点
  21. function(elem, context, xml) {
  22. var data, cache, outerCache,
  23. dirkey = dirruns + " " + doneName;
  24.  
  25. // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching
  26. if (xml) {
  27. while ((elem = elem[dir])) {
  28. if (elem.nodeType === 1 || checkNonElements) {
  29. if (matcher(elem, context, xml)) {
  30. return true;
  31. }
  32. }
  33. }
  34. } else {
  35. while ((elem = elem[dir])) {
  36.             //一直向上查找,直到找到一个为止
  37. if (elem.nodeType === 1 || checkNonElements) {
  38. outerCache = elem[expando] || (elem[expando] = {});
  39. if ((cache = outerCache[dir]) && cache[0] === dirkey) {
  40. if ((data = cache[1]) === true || data === cachedruns) {
  41. return data === true;
  42. }
  43. } else {
  44. cache = outerCache[dir] = [dirkey];
  45. cache[1] = matcher(elem, context, xml) || cachedruns; //cachedruns//正在匹配第几个元素
  46. if (cache[1] === true) {
  47. return true;
  48. }
  49. }
  50. }
  51. }
  52. }
  53. };
  54. }

有了上面两个函数的支持后,matcherFromTokens的作用就遍历tokens数组

  1. //将tokens数组转换成一个过滤函数,略过伪类部分
    function matcherFromTokens(tokens) {
  2. var checkContext, matcher, j,
  3. len = tokens.length,
  4. leadingRelative = Expr.relative[tokens[0].type],
  5. implicitRelative = leadingRelative || Expr.relative[" "],
  6. i = leadingRelative ? 1 : 0,
  7.  
  8. // The foundational matcher ensures that elements are reachable from top-level context(s)
  9. matchContext = addCombinator(function(elem) {
  10. return elem === checkContext;
  11. }, implicitRelative, true),
  12.  
  13. matchAnyContext = addCombinator(function(elem) {
  14. return indexOf.call(checkContext, elem) > -1;
  15. }, implicitRelative, true),
  16.  
  17. matchers = [
  18. function(elem, context, xml) {
  19. return (!leadingRelative && (xml || context !== outermostContext)) || (
  20. (checkContext = context).nodeType ?
  21. matchContext(elem, context, xml) :
  22. matchAnyContext(elem, context, xml));
  23. }
  24. ];
  25.  
  26. for (; i < len; i++) {
  27. // 如果是关系型选择符,执行addCombinator方法
  28. if ((matcher = Expr.relative[tokens[i].type])) {
  29. matchers = [addCombinator(elementMatcher(matchers), matcher)];
  30. } else {
  31. 否则直接找到选择器的过滤函数
  32. matcher = Expr.filter[tokens[i].type].apply(null, tokens[i].matches);
  33. matchers.push(matcher);
  34. }
  35. }
  36.  
  37. return elementMatcher(matchers);
  38. }

下面我们来看看Expr.filter,前面说过他总共有

ID:ID选择符

Class:类选择符

Tag:标签选择符

ATTR:属性标签

CHILD:包括(only|first|last|nth|nth-last)-(child|of-type)等等对子类的标签

PSEUDO:其他伪类选择符

这几种类型,那Expr.filter里面包含的分别是各种类型的过滤函数,比如Expr.filter["ID"]

  1.       Expr.filter["ID"] = function(id) {
  2. var attrId = id.replace(runescape, funescape);
  3. return function(elem) {
  4. return elem.getAttribute("id") === attrId;
  5. };
  6. };

这个函数的作用就是通过参数ID返回新的函数FUNC_ID,这个函数参数传入一个DOM元素(其实就是之前的seed集合),判断这个DOM元素的ID是否是指定ID,也就是判断seed集合是否是选择符指定的ID元素。

  1.       "TAG": function(nodeNameSelector) {
  2. var nodeName = nodeNameSelector.replace(runescape, funescape).toLowerCase();
  3. return nodeNameSelector === "*" ?
  4. function() {
  5. return true;
  6. } :
  7. function(elem) {
  8. return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
  9. };
  10. },

TAG的函数的作用就是通过参数nodeNameSelector生成一个新的函数FUNC_NODE,这个函数判断传入的DOM元素是否是指定的nodeNameSelector类型标签。

总之就是一组过滤函数,判断DOM节点是否符合选择符的条件,满足就留下,否则剔除掉。

  1.      
               
    终于要对我们的Seed进行过滤了!前面我们通过matcherFromTokens方法生成了一个包含所有选择符过滤函数的统一过滤函数,下面还需要对seed集合进行挨个过滤,就是matcherFromGroupMatchers要做的事情:
    matcherFromGroupMatchers函数主要针对伪类和非伪类综合处理,我们暂不考虑伪类情况matcherFromGroupMatchers可以简化许多:
    可以看到整个代码最关键的地方就是有一个双层循环,把所有的seed集合拿出来对所有的过滤函数进行执行,把返回true的集合保留下来,就是我们最终要查询的结果:
  1. function matcherFromGroupMatchers(elementMatchers) {
  2.  
  3. var matcherCachedRuns = 0,
  4. byElement = elementMatchers.length > 0,
  5.  
  6. return function(seed, context, xml, results, expandContext) {
  7. var elem, j, matcher,
  8. setMatched = [],
  9. i = "0",
  10. unmatched = seed && [],
  11. outermost = expandContext != null,
  12. contextBackup = outermostContext,
  13.  
  14. //可以看到如果没有seed集合就会把所有的DOM节点查询出来当做seed (Expr.find["TAG"]("*")) 所以我们在写选择符的时候最好不要在末尾写*
  15. // We must always have either seed elements or context
  16. elems = seed || byElement && Expr.find["TAG"]("*", expandContext && context.parentNode || context),
  17. // Use integer dirruns iff this is the outermost matcher
  18. dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),
  19. len = elems.length;
  20.  
  21. if (outermost) {
  22. outermostContext = context !== document && context;
  23. cachedruns = matcherCachedRuns;
  24. }
  25.  
  26. //
  27. // Add elements passing elementMatchers directly to results
  28. // Keep `i` a string if there are no elements so `matchedCount` will be "00" below
  29. // Support: IE<9, Safari
  30. // Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id
             //遍历所有seed节点
  31. for (; i !== len && (elem = elems[i]) != null; i++) {
  32. if (byElement && elem) {
  33. j = 0;
  34.  
  35. //遍历所有过滤函数
  36. while ((matcher = elementMatchers[j++])) {
  37. //
  38. if (matcher(elem, context, xml)) {
  39. results.push(elem);
  40. break;
  41. }
  42. }
  43. if (outermost) {
  44. dirruns = dirrunsUnique;
  45. cachedruns = ++matcherCachedRuns;
  46. }
  47. }
  48. }
  49.  
  50. // Override manipulation of globals by nested matchers
  51. if (outermost) {
  52. dirruns = dirrunsUnique;
  53. outermostContext = contextBackup;
  54. }
  55.  
  56. return unmatched;
  57. };
  58. }
  1. 至此,$("#div_test > span input[checked=true]") 从头到尾的流程就基本走通了。为此我们可以得出几个优化选择器的结论:
  2.  
  3. 1. 尽量在选择器以ID来查询,或者至少开头是以ID来查询:这样可以快速缩小查询的根节点。

    2. Classe前面使用Tags:因为getElementsByTagName方法是第二快的查询方法
  4. 3. 在选择器最后尽量指定seed元素(千万不能用*):因为Sizzle会从最后的选择符开始寻找符合条件的seed集合
  5. 4. 尽量使用父子查询来代替后代查询:后代查询需要循环查找,父子查询范围小很多。
  6. 5. 缓存已查询的jQuery对象:通过空间换时间的方式,不要每次都要执行过滤函数。
  1.  

Sizzle源码分析:三 筛选和编译的更多相关文章

  1. 使用react全家桶制作博客后台管理系统 网站PWA升级 移动端常见问题处理 循序渐进学.Net Core Web Api开发系列【4】:前端访问WebApi [Abp 源码分析]四、模块配置 [Abp 源码分析]三、依赖注入

    使用react全家桶制作博客后台管理系统   前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基 ...

  2. tomcat源码分析(三)一次http请求的旅行-从Socket说起

    p { margin-bottom: 0.25cm; line-height: 120% } tomcat源码分析(三)一次http请求的旅行 在http请求旅行之前,我们先来准备下我们所需要的工具. ...

  3. Sizzle源码分析 (一)

    Sizzle 源码分析 (一) 2.1 稳定 版本 Sizzle 选择器引擎博大精深,下面开始阅读它的源代码,并从中做出标记 .先从入口开始,之后慢慢切入 . 入口函数 Sizzle () 源码 19 ...

  4. Sizzle源码分析:一 设计思路

    一.前言 DOM选择器(Sizzle)是jQuery框架中非常重要的一部分,在H5还没有流行起来的时候,jQuery为我们提供了一个简洁,方便,高效的DOM操作模式,成为那个时代的经典.虽然现在Vue ...

  5. Duilib源码分析(三)XML解析器—CMarkup

    上一节介绍了控件构造器CDialogBuilder,接下来将分析其XML解析器CMarkup: CMarkup:xml解析器,目前内置支持三种编码格式:UTF8.UNICODE.ASNI,默认为UTF ...

  6. sizzle源码分析 (4)sizzle 技术总结及值得我们学习的地方

    分析sizzle源码并不是为了去钻牛角尖,而是去了解它的思想,学习下期中一些技术的运用. 1,sizzle中的正则表达式jquery源码中充斥着各种正则表达式,能否看懂其源码的关键之一就是对正则表达式 ...

  7. Spring5深度源码分析(三)之AnnotationConfigApplicationContext启动原理分析

    代码地址:https://github.com/showkawa/spring-annotation/tree/master/src/main/java/com/brian AnnotationCon ...

  8. Vue.js 源码分析(三) 基础篇 模板渲染 el、emplate、render属性详解

    Vue有三个属性和模板有关,官网上是这样解释的: el ;提供一个在页面上已存在的 DOM 元素作为 Vue 实例的挂载目标 template ;一个字符串模板作为 Vue 实例的标识使用.模板将会 ...

  9. 5.2 spring5源码--spring AOP源码分析三---切面源码分析

    一. AOP切面源码分析 源码分析分为三部分 1. 解析切面 2. 创建动态代理 3. 调用 源码的入口 源码分析的入口, 从注解开始: 组件的入口是一个注解, 比如启用AOP的注解@EnableAs ...

  10. ABP源码分析三:ABP Module

    Abp是一种基于模块化设计的思想构建的.开发人员可以将自定义的功能以模块(module)的形式集成到ABP中.具体的功能都可以设计成一个单独的Module.Abp底层框架提供便捷的方法集成每个Modu ...

随机推荐

  1. python笔记9-多线程Threading之阻塞(join)和守护线程(setDaemon)

    python笔记9-多线程Threading之阻塞(join)和守护线程(setDaemon) 前言 今天小编YOYO请xiaoming和xiaowang吃火锅,吃完火锅的时候会有以下三种场景: - ...

  2. Mirror--镜像相关操作

    其他相关操作1. 关闭镜像--关闭镜像USE [master]GOALTER DATABASE Demo1 SET PARTNER OFFGO  2. 证服务器--移除见证服务器USE [master ...

  3. git-【十】忽略文件

    1.在Git工作区的根目录下创建一个特殊的.gitignore文件,然后把要忽略的文件名填进去,Git就会自动忽略这些文件. 不需要从头写.gitignore文件,GitHub已经为我们准备了各种配置 ...

  4. AE Scene开发中的观察者模式

    AE SceneGraph中的观察者模式 注意SceneControl不是观察者,它只是一个SceneGraph的拥有者:SceneViewer才是观察者,SceneGraph是被观察对象,同时观察者 ...

  5. OpenCV Windows7 VC6.0安装以及HelloWorld

    anna在实验室配置OpenCV的时候,按照中文网站的介绍,很顺利的就完成了.可是回到家情况就大不一样!!总是在链接的时候报错,不是少这个lib就是少那个lib大哭最后查明是anna马虎,忘了将C:\ ...

  6. docker——核心实现技术

    作为一种容器虚拟化技术,Docker深度应用了操作系统的多项底层支持技术. 早期版本的Docker是基于已经成熟的Linux Container(LXC)技术实现的.自从0.9版本起,Docker逐渐 ...

  7. 忘记oracle的sys用户密码怎么修改以及Oracle 11g 默认用户名和密码

    欢迎和大家交流技术相关问题: 邮箱: jiangxinnju@163.com 博客园地址: http://www.cnblogs.com/jiangxinnju GitHub地址: https://g ...

  8. iOS App迁移(App Transfer)注意点

    1.App迁移需要苹果审核吗? 答:不需要 2.App迁移需要多长时间? 答:迁移操作过程很快,A账号发出申请,B账号接收,几分钟时间.App Store 展示B账号相关信息可能几分钟,也可能有延迟几 ...

  9. TED #02#

    Amanda Palmer: The art of asking 1. I think people have been obsessed with the wrong question, which ...

  10. bzoj1612 / P2419 [USACO08JAN]牛大赛Cow Contest(Floyd)

    P2419 [USACO08JAN]牛大赛Cow Contest Floyd不仅可以算最短路,还可以处理点之间的关系. 跑一遍Floyd,处理出每个点之间是否有直接或间接的关系. 如果某个点和其他$n ...