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字典探测用户名工具
Oct 05 Javascript
ExtJS判断IE浏览器类型的方法
Feb 10 Javascript
js正则表达式中exec用法实例
Jul 23 Javascript
浅谈JavaScript中setInterval和setTimeout的使用问题
Aug 01 Javascript
以JavaScript来实现WordPress中的二级导航菜单的方法
Dec 14 Javascript
JavaScript 浏览器兼容性总结及常用浏览器兼容性分析
Mar 30 Javascript
浅谈javascript中的Function和Arguments
Aug 30 Javascript
Angular中$state.go页面跳转并传递参数的方法
May 09 Javascript
React-Native之定时器Timer的实现代码
Oct 04 Javascript
Vue2.0 slot分发内容与props验证的方法
Dec 12 Javascript
JS前端知识点offset,scroll,client,冒泡,事件对象的应用整理总结
Jun 27 Javascript
微信小程序 接入腾讯地图的两种写法
Jan 12 Javascript
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数组一对一替换实现代码
2012/08/31 PHP
php导出word文档与excel电子表格的简单示例代码
2014/03/08 PHP
laravel实现一个上传图片的接口,并建立软链接,访问图片的方法
2019/10/12 PHP
checkbox 复选框不能为空
2009/07/11 Javascript
JS实现随机数生成算法示例代码
2013/08/08 Javascript
浅析jQuery1.8的几个小变化
2013/12/10 Javascript
jQuery移除tr无效的解决方法(tr是动态添加)
2014/09/22 Javascript
JS+CSS实现精美的二级导航效果代码
2015/09/17 Javascript
apply和call方法定义及apply和call方法的区别
2015/11/15 Javascript
jQuery Mobile框架中的表单组件基础使用教程
2016/05/17 Javascript
jQuery动态创建元素以及追加节点的实现方法
2016/10/20 Javascript
详解jQuery简单的表单应用
2016/12/16 Javascript
layui弹出层效果实现代码
2017/05/19 Javascript
JS按钮闪烁功能的实现代码
2017/07/21 Javascript
网页爬虫之cookie自动获取及过期自动更新的实现方法
2018/03/06 Javascript
关于小程序优化的一些建议(小结)
2020/12/10 Javascript
[51:15]完美世界DOTA2联赛PWL S2 PXG vs Magma 第一场 11.21
2020/11/24 DOTA
python pickle 和 shelve模块的用法
2013/09/16 Python
Python实现比较两个文件夹中代码变化的方法
2015/07/10 Python
python实现自动解数独小程序
2019/01/21 Python
python 求某条线上特定x值或y值的点坐标方法
2019/07/09 Python
Django Rest framework权限的详细用法
2019/07/25 Python
tensorflow 重置/清除计算图的实现
2020/01/19 Python
python爬虫开发之Request模块从安装到详细使用方法与实例全解
2020/03/09 Python
jupyter note 实现将数据保存为word
2020/04/14 Python
python实现视频压缩功能
2020/12/18 Python
澳大利亚首屈一指的鞋类品牌:Tony Bianco
2018/03/13 全球购物
Ellos瑞典官网:北欧地区时尚、美容和住宅领域领先的电子商务网站
2019/11/21 全球购物
Shell脚本如何向终端输出信息
2014/04/25 面试题
应届毕业生个人自我评价
2013/09/20 职场文书
竞选班长演讲稿
2013/12/30 职场文书
小学生打架检讨书
2014/01/26 职场文书
销售员岗位职责范本
2014/02/03 职场文书
争先创优演讲稿
2014/09/15 职场文书
简历自荐信范文
2015/03/09 职场文书
2016年3月份红领巾广播稿
2015/12/21 职场文书