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 相关文章推荐
IE下使用cloneNode注意事项分享
Nov 22 Javascript
js实现window.open不被拦截的解决方法汇总
Oct 30 Javascript
创建你的第一个AngularJS应用的方法
Jun 16 Javascript
d3.js实现简单的网络拓扑图实例代码
Nov 06 Javascript
读Javascript高性能编程重点笔记
Dec 21 Javascript
JS实现的四级密码强度检测功能示例
May 11 Javascript
浅谈react.js中实现tab吸顶效果的问题
Sep 06 Javascript
200行代码实现blockchain 区块链实例详解
Mar 14 Javascript
关于JavaScript中高阶函数的魅力详解
Sep 07 Javascript
node.js实现简单的压缩/解压缩功能示例
Nov 05 Javascript
15个简单的JS编码标准让你的代码更整洁(小结)
Jul 16 Javascript
详解vue中v-for的key唯一性
May 15 Vue.js
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
PHP小技巧搜集,每个PHPer都来露一手
2007/01/02 PHP
PHP的消息通信机制测试实例
2016/11/10 PHP
详谈PHP中public,private,protected,abstract等关键字的用法
2017/12/31 PHP
Laravel 微信小程序后端搭建步骤详解
2019/11/26 PHP
用javascript实现兼容IE7的类库 IE7_0_9.zip提供下载
2007/08/08 Javascript
JavaScript 图像动画的小demo
2012/05/23 Javascript
常见的原始JS选择器使用方法总结
2014/04/09 Javascript
javascript实现检验的各种规则
2015/07/31 Javascript
jQuery zclip插件实现跨浏览器复制功能
2015/11/02 Javascript
JS动态插入并立即执行回调函数的方法
2016/04/21 Javascript
JavaScript版经典游戏之扫雷游戏完整示例【附demo源码下载】
2016/12/12 Javascript
Node.js 回调函数实例详解
2017/07/06 Javascript
Javascript中 toFixed四舍六入方法
2017/08/21 Javascript
纯JS实现出生日期[年月日]下拉菜单效果
2018/06/01 Javascript
深入理解Angularjs 脏值检测
2018/10/12 Javascript
p5.js实现故宫橘猫赏秋图动画
2019/10/23 Javascript
js实现百度淘宝搜索功能
2020/02/17 Javascript
[45:52]2018DOTA2亚洲邀请赛 4.1小组赛 A组加赛 LGD vs Liquid
2018/04/02 DOTA
python paramiko实现ssh远程访问的方法
2013/12/03 Python
python字符类型的一些方法小结
2016/05/16 Python
python 调用win32pai 操作cmd的方法
2017/05/28 Python
Python-OpenCV基本操作方法详解
2018/04/02 Python
对python借助百度云API对评论进行观点抽取的方法详解
2019/02/21 Python
python关于调用函数外的变量实例
2019/12/26 Python
iPython pylab模式启动方式
2020/04/24 Python
python3通过qq邮箱发送邮件以及附件
2020/05/20 Python
德国传统玻璃制造商:Cristalica
2018/04/23 全球购物
时尚、社区、科技:SEVENSTORE
2019/04/26 全球购物
甜品店的创业计划书范文
2014/01/02 职场文书
公司请假条范文
2014/04/11 职场文书
行政专员求职信范文
2014/05/03 职场文书
小学教师师德承诺书
2014/05/23 职场文书
优秀党支部书记事迹材料
2014/05/29 职场文书
建筑工地大门标语
2014/06/18 职场文书
2015年党员创先争优公开承诺书
2015/04/27 职场文书
2015年女工委工作总结
2015/07/27 职场文书