jQuery源码分析之sizzle选择器详解


Posted in Javascript onFebruary 13, 2017

前言

Sizzle 原本是 jQuery 中用来当作 DOM 选择器的,后来被 John Resig 单独分离出去,成为一个单独的项目,可以直接导入到项目中使用。

点击这里下:jquery/sizzle。

本来我们使用 jQuery 当作选择器,选定一些 #id 或 .class,使用 document.getElementById document.getElemensByClassName 就可以很快锁定 DOM 所在的位置,然后返回给 jQuery 当作对象。但有时候会碰到一些比较复杂的选择 div div.hot>span 这类肯定用上面的函数是不行的,首先考虑到的是 Element.querySelectorAll() 函数,但这个函数存在严重的兼容性问题MDN querySelectorAll。这个时候 sizzle 就派上用场了。

init 函数介绍中已经说明白,没有介绍 find 函数,其本质上就是 Sizzle 函数在 jQuery 中的表现。

这个函数在 jQuery 中两种存在形式,即原型和属性上分别有一个,先来看下 jQuery.fn.find:

jQuery.fn.find = function (selector) {
 var i, ret, len = this.length,
 self = this;
 // 这段话真不知道是个什么的
 if (typeof selector !== "string") {
 // fn.pushStack 和 jquery.merge 很像,但是返回一个 jquery 对象,且
 // jquery 有个 prevObject 属性指向自己
 return this.pushStack(jQuery(selector).filter(function () {
  for (i = 0; i < len; i++) {
  // jQuery.contains(a, b) 判断 a 是否是 b 的父代
  if (jQuery.contains(self[i], this)) {
   return true;
  }
  }
 }));
 }

 ret = this.pushStack([]);

 for (i = 0; i < len; i++) {
 // 在这里引用到 jQuery.find 函数
 jQuery.find(selector, self[i], ret);
 }
 // uniqueSort 去重函数
 return len > 1 ? jQuery.uniqueSort(ret) : ret;
}

jQuery.fn.find 的用法一般在 $('.test').find("span") ,所以此时的 this 是指向 $(‘.test') 的,懂了这一点,后面的东西自然而然就好理解了。

然后就是 jQuery.find 函数,本章的重点讨论部分。

先来看一个正则表达式:

var rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/;
rquickExpr.exec('#id') //["#id", "id", undefined, undefined]
rquickExpr.exec('div') //["div", undefined, "div", undefined]
rquickExpr.exec('.test') //[".test", undefined, undefined, "test"]
rquickExpr.exec('div p')// null

你可能会疑惑,rquickExpr 的名字已经出现过一次了。实际上 Sizzle 是一个闭包,这个 rquickExpr 变量是在 Sizzle 闭包内的,不会影响到 jQuery 全局。这个正则的作用主要是用来区分 tag、id 和 class,而且从返回的数组也有一定的规律,可以通过这个规律来判断 selector 具体是哪一种。

jQuery.find = Sizzle;

function Sizzle(selector, context, results, seed) {
 var m, i, elem, nid, match, groups, newSelector, newContext = context && context.ownerDocument,

 // nodeType defaults to 9, since context defaults to document
 nodeType = context ? context.nodeType : 9;

 results = results || [];

 // Return early from calls with invalid selector or context
 if (typeof selector !== "string" || !selector || nodeType !== 1 && nodeType !== 9 && nodeType !== 11) {

 return results;
 }

 // Try to shortcut find operations (as opposed to filters) in HTML documents
 if (!seed) {

 if ((context ? context.ownerDocument || context : preferredDoc) !== document) {
  // setDocument 函数其实是用来将 context 设置成 document,考虑到浏览器的兼容性
  setDocument(context);
 }
 context = context || document;
 // true
 if (documentIsHTML) {

  // match 就是那个有规律的数组
  if (nodeType !== 11 && (match = rquickExpr.exec(selector))) {

  // selector 是 id 的情况
  if ((m = match[1])) {

   // Document context
   if (nodeType === 9) {
   if ((elem = context.getElementById(m))) {

    if (elem.id === m) {
     results.push(elem);
     return results;
    }
   } else {
    return results;
   }

   // 非 document 的情况
   } else {

   if (newContext && (elem = newContext.getElementById(m)) && contains(context, elem) && elem.id === m) {

    results.push(elem);
    return results;
   }
   }

  // selector 是 tagName 情况
  } else if (match[2]) {
   // 这里的 push:var push = arr.push
   push.apply(results, context.getElementsByTagName(selector));
   return results;

  // selector 是 class 情况
  } else if ((m = match[3]) && support.getElementsByClassName && context.getElementsByClassName) {

   push.apply(results, context.getElementsByClassName(m));
   return results;
  }
  }

  // 如果浏览器支持 querySelectorAll
  if (support.qsa && !compilerCache[selector + " "] && (!rbuggyQSA || !rbuggyQSA.test(selector))) {

  if (nodeType !== 1) {
   newContext = context;
   newSelector = selector;

   // qSA looks outside Element context, which is not what we want
   // Support: IE <=8,还是要考虑兼容性
  } else if (context.nodeName.toLowerCase() !== "object") {

   // Capture the context ID, setting it first if necessary
   if ((nid = context.getAttribute("id"))) {
   nid = nid.replace(rcssescape, fcssescape);
   } else {
   context.setAttribute("id", (nid = expando));
   }

   // Sizzle 词法分析的部分
   groups = tokenize(selector);
   i = groups.length;
   while (i--) {
   groups[i] = "#" + nid + " " + toSelector(groups[i]);
   }
   newSelector = groups.join(",");

   // Expand context for sibling selectors
   newContext = rsibling.test(selector) && testContext(context.parentNode) || context;
  }

  if (newSelector) {
   try {
   push.apply(results, newContext.querySelectorAll(newSelector));
   return results;
   } catch(qsaError) {} finally {
   if (nid === expando) {
    context.removeAttribute("id");
   }
   }
  }
  }
 }
 }

 // All others,select 函数和 tokenize 函数后文再谈
 return select(selector.replace(rtrim, "$1"), context, results, seed);
}

整个分析过程由于要考虑各种因素,包括效率和浏览器兼容性等,所以看起来非常长,但是逻辑一点都不难:先判断 selector 是否是非 string,然后正则 rquickExpr 对 selector 进行匹配,获得数组依次考虑 id、tagName 和 class 情况,这些都很简单,都是单一的选择,一般用浏览器自带的函数 getElement 即可解决。遇到复杂一点的,比如 div div.show p,先考虑 querySelectorAll 函数是否支持,然后考虑浏览器兼容 IE<8。若不支持,即交给 select 函数(下章)。

Sizzle 的优势

Sizzle 使用的是从右向左的选择方式,这种方式效率更高。

浏览器在处理 html 的时候,先生成一个 DOM tree,解析完 css 之后,然后更加 css 和 DOM tess 生成一个 render tree。render tree 用于渲染,不是一一对应,如 display:none 的 DOM 就不会出现在 render tree 中。

如果从左到右的匹配方式,div div.show p

  1. 找到 div 节点,
  2. 从 1 的子节点中找到 div 且 class 为 show 的 DOM,找不到则返回上一步
  3. 从 2 的子节点中找到 p 元素,找不到则返回上一步

如果有一步找不到,向上回溯,直到遍历所有的 div,效率很低。

如果从右到左的方式,

  1. 先匹配到所有的 p 节点,
  2. 对 1 中的结果注意判断,若其父节点顺序出现 div.show 和 div,则保留,否则丢弃

因为子节点可以有若干个,而父节点只有一个,故从右向左的方式效率很高。

衍生的函数

jQuery.fn.pushStack

jQuery.fn.pushStack是一个类似于 jQuery.merge 的函数,它接受一个参数,把该参数(数组)合并到一个 jQuery 对象中并返回,源码如下:

jQuery.fn.pushStack = function (elems) {

 // Build a new jQuery matched element set
 var ret = jQuery.merge(this.constructor(), elems);

 // Add the old object onto the stack (as a reference)
 ret.prevObject = this;

 // Return the newly-formed element set
 return ret;
}

jQuery.contains

这个函数是对 DOM 判断是否是父子关系,源码如下:

jQuery.contains = function (context, elem) {
 // 考虑到兼容性,设置 context 的值
 if ((context.ownerDocument || context) !== document) {
 setDocument(context);
 }
 return contains(context, elem);
}

// contains 是内部函数,判断 DOM_a 是否是 DOM_b 的
var contains = function (a, b) {
 var adown = a.nodeType === 9 ? a.documentElement : a,
 bup = b && b.parentNode;
 return a === bup || !!(bup && bup.nodeType === 1 && (
 adown.contains ? adown.contains(bup) : a.compareDocumentPosition && a.compareDocumentPosition(bup) & 16));
}

jQuery.uniqueSort

jQuery 的去重函数,但这个去重职能处理 DOM 元素数组,不能处理字符串或数字数组,来看看有什么特别的:

jQuery.uniqueSort = function (results) {
 var elem, duplicates = [],
 j = 0,
 i = 0;

 // hasDuplicate 是一个判断是否有相同元素的 flag,全局
 hasDuplicate = !support.detectDuplicates;
 sortInput = !support.sortStable && results.slice(0);
 results.sort(sortOrder);

 if (hasDuplicate) {
 while ((elem = results[i++])) {
  if (elem === results[i]) {
   j = duplicates.push(i);
  }
 }
 while (j--) {
  // splice 用于将重复的元素删除
  results.splice(duplicates[j], 1);
 }
 }

 // Clear input after sorting to release objects
 // See https://github.com/jquery/sizzle/pull/225
 sortInput = null;

 return results;
}

sortOrder 函数如下,需要将两个函数放在一起理解才能更明白哦:

var sortOrder = function (a, b) {

 // 表示有相同的元素,设置 flag 为 true
 if (a === b) {
 hasDuplicate = true;
 return 0;
 }

 // Sort on method existence if only one input has compareDocumentPosition
 var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
 if (compare) {
 return compare;
 }

 // Calculate position if both inputs belong to the same document
 compare = (a.ownerDocument || a) === (b.ownerDocument || b) ? a.compareDocumentPosition(b) :

 // Otherwise we know they are disconnected
 1;

 // Disconnected nodes
 if (compare & 1 || (!support.sortDetached && b.compareDocumentPosition(a) === compare)) {

 // Choose the first element that is related to our preferred document
 if (a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a)) {
  return -1;
 }
 if (b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b)) {
  return 1;
 }

 // Maintain original order
 return sortInput ? (indexOf(sortInput, a) - indexOf(sortInput, b)) : 0;
 }

 return compare & 4 ? -1 : 1;
}

总结

可以说今天先对 Sizzle 开个头,任重而道远!下面就会接受 Sizzle 中的 tokens 和 select 函数。感兴趣的朋友们可以继续关注三水点靠木,希望本文的内容对大家能有一定的帮助。

Javascript 相关文章推荐
Javascript操作select方法大全[新增、修改、删除、选中、清空、判断存在等]
Sep 26 Javascript
关于javascript中的parseInt使用技巧
Sep 03 Javascript
js自执行函数的几种不同写法的比较
Aug 16 Javascript
『jQuery』名称冲突使用noConflict方法解决
Apr 22 Javascript
js的Boolean对象初始值示例
Mar 04 Javascript
js使用onmousemove和onmouseout获取鼠标坐标的方法
Mar 31 Javascript
AngularJS 日期格式化详解
Dec 23 Javascript
JavaScript进阶练习及简单实例分析
Jun 03 Javascript
浅谈jquery采用attr修改form表单enctype不起作用的问题
Nov 25 Javascript
Node.js开发教程之基于OnceIO框架实现文件上传和验证功能
Nov 30 Javascript
Angular2 路由问题修复详解
Mar 01 Javascript
Jquery让form表单异步提交代码实现
Nov 14 jQuery
JS中input表单隐藏域及其使用方法
Feb 13 #Javascript
浅谈键盘上回车按钮的js触发事件
Feb 13 #Javascript
jQuery源码分析之init的详细介绍
Feb 13 #Javascript
AngulerJS学习之按需动态加载文件
Feb 13 #Javascript
JavaScript获取ul中li个数的方法
Feb 13 #Javascript
JavaScript中数组的各种操作的总结(必看篇)
Feb 13 #Javascript
JS实现选定指定HTML元素对象中指定文本内容功能示例
Feb 13 #Javascript
You might like
解析coreseek for sphinx的使用
2013/06/21 PHP
PHP实践教程之过滤、验证、转义与密码详解
2017/07/24 PHP
javascript之学会吝啬 精简代码
2010/04/25 Javascript
Javascript中引用示例介绍
2014/02/21 Javascript
当前流行的JavaScript代码风格指南
2014/09/10 Javascript
jquery实现超简洁的TAB选项卡效果代码
2015/08/28 Javascript
Jquery左右滑动插件之实现超级炫酷动画效果附源码下载
2015/12/02 Javascript
详解Webwork中Action 调用的方法
2016/02/02 Javascript
javascript比较语义化版本号的实现代码
2016/09/09 Javascript
JS判断来路是否是百度等搜索索引进行弹窗或自动跳转的实现代码
2016/10/09 Javascript
js 转义字符及URI编码详解
2017/02/28 Javascript
JS实现含有中文字符串的友好截取功能分析
2017/03/13 Javascript
用node开发并发布一个cli工具的方法步骤
2019/01/03 Javascript
vue中使用mxgraph的方法实例代码详解
2019/05/17 Javascript
scrapyd schedule.json setting 传入多个值问题
2019/08/07 Javascript
python正则匹配查询港澳通行证办理进度示例分享
2013/12/27 Python
pycharm 使用心得(三)Hello world!
2014/06/05 Python
浅谈Python的文件类型
2016/05/30 Python
Python实现简易端口扫描器代码实例
2017/03/15 Python
python实现Virginia无密钥解密
2019/03/20 Python
python批量修改图片尺寸,并保存指定路径的实现方法
2019/07/04 Python
python3.7实现云之讯、聚合短信平台的短信发送功能
2019/09/26 Python
执行Django数据迁移时报 1091错误及解决方法
2019/10/14 Python
python3中numpy函数tile的用法详解
2019/12/04 Python
django admin管理工具自定义时间区间筛选器DateRangeFilter介绍
2020/05/19 Python
Python闭包及装饰器运行原理解析
2020/06/17 Python
压铸汽车模型收藏家:Diecastmodelswholesale.com
2016/12/21 全球购物
英国乡村时尚和宠物用品专家:Pet & Country
2018/07/02 全球购物
巴西独家产品和现场演示购物网站:Shoptime
2019/07/11 全球购物
垃圾回收的优点和原理
2014/05/16 面试题
《美丽的丹顶鹤》教学反思
2014/04/22 职场文书
社会公德演讲稿
2014/05/20 职场文书
课例研修方案
2014/05/31 职场文书
勿忘国耻9.18演讲稿(经典篇)
2014/09/14 职场文书
民用住房租房协议书
2014/10/29 职场文书
2014年机关党建工作总结
2014/11/11 职场文书