jQuery源码分析(二)——Sizzle

在这一章中我们将重点分析jquery的选择器引擎。jquery在3.4版本后,将选择器引擎抽取出来单独放到了Sizzle.js 文件中,本文将基于这个版本来进行分析。

创建缓存

// line 40 创建缓存
classCache = createCache(),
tokenCache = createCache(),
compilerCache = createCache(),
nonnativeSelectorCache = createCache(),

// line 360
/**
 * Create key-value caches of limited size
 * @returns {function(string, object)} Returns the Object data after storing it on itself with
 *	property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
 *	deleting the oldest entry
 *  创建缓存对象,如果超过缓存限制大小,最删除最早加入的对象
 */
function createCache() {
	var keys = [];

	function cache( key, value ) {
		// Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
		if ( keys.push( key + " " ) > Expr.cacheLength ) {
			// Only keep the most recent entries
			delete cache[ keys.shift() ];
		}
		return (cache[ key + " " ] = value);
	}
	return cache;
}

这里有三行代码做了两次运算,掌握这些技巧可以使我们的代码更简洁。但前提是,这些方法一般是供内部使用的。

if ( keys.push( key + " " ) > Expr.cacheLength ){...}
// 这里首先将元素放入数组然后判断数组的长度是否大于阈值

delete cache[ keys.shift() ];
// 首先调用shift方法删除数组的第一个元素,并利用该方法会返回元素值,
// 再用delete 方法删除cache对象中存储的键值对

return (cache[ key + " " ] = value);
// 将赋值运算的结果返回

boolean 属性

// line 71
booleans = "checked|selected|async|autofocus|autoplay|controls|defer|"
+"disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",

里面有一些是不常用的属性或是 html5 新增的属性:

  1. async - 当 script 标签设置 async=“async” 时,浏览器解析的 DOM 的同时会加载并执行脚本
  2. defer - 当 dom 加载结束后加载并执行 js
  3. autoplay - 对应于 video 标签,视频是否自动播放
  4. controls - 对应于 video 标签,是否显示暂停、播放组件
  5. controls - 对应于 video 标签,是否开启循环播放
  6. ismap - 对应于 img 标签,是否开启图像映射
  7. multiple - 对应于 input 标签(type=file),是否允许上传多个文件
  8. open - 对应于 details 标签,details区域是否可见
  9. scoped - 对应于 style 标签,如果使用该属性,则样式仅仅应用到 style 元素的父元素及其子元素。

空格符

whitespace = "[\\x20\\t\\r\\n\\f]",
  1. \x20 为空格符
  2. \t 为制表符 (tabs)
  3. \r 为回车符 (Carriage Return)
  4. \n 为换行符 (Line Feed)
  5. \f 为换页符 (Form feed)

连接符

	var rcombinators = new RegExp("^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*");

该正则表达式表明jquery中标识关系的连接符有如下几种:

  1. > 在给定的父元素下匹配所有的子元素。用以匹配元素的选择器,并且它是第一个选择器的子元素。
  2. + 匹配所有紧接在 prev 元素后的 next 元素。一个有效选择器并且紧接着第一个选择器。
  3. ~ 匹配 prev 元素之后的所有 siblings 元素。一个选择器,并且它作为第一个选择器的同辈。注意这里是之后的,就是说在prev元素之后的同级元素,而prev元素之前的同级元素是不会被选中的。
  4. 在给定的祖先元素下匹配所有的后代元素。
	var rdescend = new RegExp(whitespace + "|>");

>(空格)都表示选取父级元素的后代,不同的是>选取直接后代,后代的后代不会被选中;而 (空格)会选取所有的子级元素。

HTML 代码:
<form>
  <label>Name:</label>
  <input name="name" />
  <fieldset>
      <label>Newsletter:</label>
      <input name="newsletter" />
 </fieldset>
</form>
<input name="none" />
jquery 代码
$("form > input")
结果
[ <input name="name" />]
jquery 代码
$("form input")
结果
[ <input name="name" />, <input name="newsletter" /> ]

字符转义

funescape = function( _, escaped, escapedWhitespace ) {
	var high = "0x" + escaped - 0x10000;
	// NaN means non-codepoint
	// Support: Firefox<24
	// Workaround erroneous numeric interpretation of +"0x"
	return high !== high || escapedWhitespace ?
		escaped :
		high < 0 ?
			// BMP codepoint
			String.fromCharCode( high + 0x10000 ) :
			// Supplemental Plane codepoint (surrogate pair)
			String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
},

该方法在jquery源码中的9处地方被调用,均用于替换字符,如:

var attrId = id.replace( runescape, funescape );

因为NaN!==NaN ,我们可以利用这个特性,通过high !== high来判断high是否是NaN

BMP (Basic Multilingual Plane)即基本多语言面。任意一个字符都可以用Unicode字符集中的数字来唯一标识,比如,字母“A”的编码为0041;字符“€”的编码为20AC。Unicode标准始终使用十六进制数字,而且在书写时在前面加上前缀“U+”,所以“A”的编码书写为“U+0041”。codepoint,代码点是指可用于编码字符集的数字。编码字符集定义一个有效的代码点范围,但是并不一定将字符分配给所有这些代码点。有效的 Unicode代码点范围是 U+0000 至 U+10FFFF。

16 位编码的所有65536个字符并不能完全表示全世界所有正在使用或曾经使用的字符。于是,Unicode 标准已扩展到包含多达 1112064 个字符。那些超出原来的16 位限制的字符被称作增补字符。

javascript中字符类型在内存中固定占16位(两个字节,不同编码占的空间不同)。代码点在U+0000 — U+FFFF之内到是可以用一个char完整的表示出一个字符。但代码点在U+FFFF之外的,一个char无论如何无法表示一个完整字符。这样用char类型来获取字符串中的那些代码点在U+FFFF之外的字符就会出现问题。增补字符是代码点在 U+10000 至 U+10FFFF 范围之间的字符,也就是那些使用原始的 Unicode 的 16 位设计无法表示的字符。从 U+0000 至 U+FFFF 之间的字符集有时候被称为基本多语言面 (BMP UBasic Multilingual Plane)。因此,每一个 Unicode 字符要么属于 BMP,要么属于增补字符。

因此在上面的代码中如果 high 小于 0 说明该字符属于 BMP ,如果大于 0 则属于增补字符。

>> 为移位运算符,参见下面的例子

// 二进制 1000
var a = 8
var b = a>>1;
// 4  即 100
var c = a<<2;
// 16 即 10000

& 为按位与运算, | 为按位或运算。就是将参与运算的两个数字转成二进制然后按位运算,参见下面的例子:

运算表达式二进制十进制
按位与10&11101010
101111
101010
7&801117
10008
00000
按位或10|11101010
101111
101111
7|801117
10008
111115
按位异或10^11101010
101111
00011
7^801117
10008
111115

Unicode编码 参考文档

处理css标识符

// CSS string/identifier serialization
// https://drafts.csswg.org/cssom/#common-serializing-idioms
// 主要用来处理css标识符存在‘\’的问题
rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,
fcssescape = function (ch, asCodePoint) {
	if (asCodePoint) {

		// U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER
		if (ch === "\0") {
			return "\uFFFD";
		}

		// Control characters and (dependent upon position) numbers get escaped as code points
		return ch.slice(0, -1) + "\\" + ch.charCodeAt(ch.length - 1).toString(16) + " ";
	}

	// Other potentially-special ASCII characters get backslash-escaped
	return "\\" + ch;
},

由于后面的代码中经常遇到nodeType的判断,因此我们先介绍一下关于nodeType。

nodeType

nodeType 属性可用来区分不同类型的节点,比如 元素, 文本注释

常量描述
Node.ELEMENT_NODE1一个 元素 节点,例如 <p><div>
Node.TEXT_NODE3Element 或者 Attr 中实际的  文字
Node.PROCESSING_INSTRUCTION_NODE7一个用于XML文档的 ProcessingInstruction ,例如 <?xml-stylesheet ... ?> 声明。
Node.COMMENT_NODE8一个 Comment 节点。
Node.DOCUMENT_NODE9一个 Document 节点。
Node.DOCUMENT_TYPE_NODE10描述文档类型的 DocumentType 节点。例如 <!DOCTYPE html>  就是用于 HTML5 的。
Node.DOCUMENT_FRAGMENT_NODE11一个 DocumentFragment 节点
示例
<!DOCTYPE html>
<html>
    <head></head>
    <body>
		<!-- This is a comment -->				
        <div id="test">123<span>456</span></div>
		<iframe src="https://www.csdn.net/"></iframe>
		this is text
	</body>        
	<script>
		var body = document.getElementsByTagName('body')[0];
		var comment = body.childNodes[1];
		var div = body.childNodes[3];
		var frame = body.childNodes[5];
		var text = body.childNodes[6];
		var fragment = document.createDocumentFragment()
		console.log(document.childNodes[0],document.childNodes[0].nodeType);
		/* <!DOCTYPE html> 10 */
		console.log(fragment,fragment.nodeType);
		/* #document-fragment 11 */
		console.log(comment,comment.nodeType);
		/* <!-- This is a comment --> 8	*/	
		console.log(div,div.nodeType);
		/* <div id="test">...</div> 1 */
		console.log(frame,frame.nodeType);
		/* <iframe src="https://www.csdn.net/">...</iframe> 1 */
		console.log(text,text.nodeType);
		/* "this is text" 3  */
	</script>
</html>

markFunction

	/**
	 * Mark a function for special use by Sizzle
	 * 用Sizzle标记一个特殊用途的函数
	 * @param {Function} fn The function to mark //待标记的函数
	 */
	function markFunction(fn) {
		fn[expando] = true;
		return fn;
	}

assert

该方法主要用来判断浏览器对接口是否支持

    /**
	 * Support testing using an element
	 * 支持使用元素进行测试
	 * @param {Function} fn Passed the created element and returns a boolean result
	 */
	function assert(fn) {
		var el = document.createElement("fieldset");

		try {
			return !!fn(el);
		} catch (e) {
			return false;
		} finally {
			// Remove from its parent by default
			if (el.parentNode) {
				el.parentNode.removeChild(el);
			}
			// release memory in IE
			el = null;
		}
	}

addHandle

这个方法主要是为了解决跨浏览器的兼容问题,通常是和asset()函数一起使用的。

	/**
	 * Adds the same handler for all of the specified attrs
	 * 为所有指定的attrs添加相同的处理程序
	 * @param {String} attrs Pipe-separated list of attributes
	 * @param {Function} handler The method that will be applied
	 */
	function addHandle(attrs, handler) {
		var arr = attrs.split("|"),
			i = arr.length;

		while (i--) {
			Expr.attrHandle[arr[i]] = handler;
		}
	}

siblingCheck

该方法用来辅助元素排序,详见sortOrder方法(line:883)

	/**
	 * Checks document order of two siblings
	 * 检查两个兄弟文档的顺序
	 * @param {Element} a
	 * @param {Element} b
	 * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
	 * 如果小于0 则a在b前面,如果大于0则a在b后面
	 */
	function siblingCheck(a, b) {
		var cur = b && a,
			diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
				a.sourceIndex - b.sourceIndex;

		// Use IE sourceIndex if available on both nodes
		// sourceIndex 为IE下元素的属性,用来标记元素的索引,从1开始。
		if (diff) {
			return diff;
		}

		// Check if b follows a
		if (cur) {
			while ((cur = cur.nextSibling)) {
				if (cur === b) {
					return -1;
				}
			}
		}

		return a ? 1 : -1;
	}

我们先跳过中间大段的函数和对象定义,先从整体上看一下Sizzle的结构。将代码直接拉到 2201 行,按照代码执行的顺序继续读代码。

	// One-time assignments
	// Sort stability
	support.sortStable = expando.split("").sort(sortOrder).join("") === expando;

support 对象是在第14行就进行了声明,在第555行进行了赋值

	// Expose support vars for convenience
	// 为了方便使用,将support变量暴露出来
	support = Sizzle.support = {};

support对象里面定义个是否支持各种“特性”。

sortable
    var hasDuplicate,
        sortOrder = function (a, b) {
			if (a === b) {
				hasDuplicate = true;
			}
			return 0;
		};
	// Sort stability
	support.sortStable = expando.split("").sort(sortOrder).join("") === expando;

检测当前浏览器是否支持自定义排序。

arrayObject.sort(sortby);

function sortby(a, b){
    return a - b; // 升序
    //return b - a; // 降序
    //retrun 0; // 顺序不变   
}

sortDetached

support.sortDetached = assert(function (el) {
		// Should return 1, but returns 4 (following)
		return el.compareDocumentPosition(document.createElement("fieldset")) & 1;
	});

compareDocumentPosition() 方法比较两个节点,并返回描述它们在文档中位置的整数。

常量名十进制值含义
DOCUMENT_POSITION_DISCONNECTED1不在同一文档中
DOCUMENT_POSITION_PRECEDING2otherNode在node之前
DOCUMENT_POSITION_FOLLOWING4otherNode在node之后
DOCUMENT_POSITION_CONTAINS8otherNode包含node
DOCUMENT_POSITION_CONTAINED_BY16otherNode被node包含
DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC32待定

参见张鑫旭写的 深入Node.compareDocumentPosition API 里面对compareDocumentPosition()这个接口解释的比较清楚。

回到本例中,这里调用了assert函数,让我们再看看它的代码:


	/**
	 * Support testing using an element
	 * @param {Function} fn Passed the created element and returns a boolean result
	 */
	function assert(fn) {
		var el = document.createElement("fieldset");

		try {
			return !!fn(el);
		} catch (e) {
			return false;
		} finally {
			// Remove from its parent by default
			if (el.parentNode) {
				el.parentNode.removeChild(el);
			}
			// release memory in IE
			el = null;
		}
	}

结合前面的代码可以简化如下:

support.sortDetached = function() {
		var el = document.createElement("fieldset");
		var el2 = document.createElement("fieldset");
		var compareResult = el.compareDocumentPosition(el2); // compareResult = 37
		var relation = compareResult & 1; // relation = 1
		return !!relation; // true 
	}

chrome 74下 代码运行的中间结果如注释中所示。
这里 compareResult = 37 = 32 + 4 + 1;即,el1和el2都在内存中,是一种特殊情况,el2位于el之后,el与el2并不在同一文档中。
通过张鑫旭的博客大家可以知道,compareResult实际上只是一个位掩码,所以必须再使用按位与运算符才能得到有意义的值。
compareResult & 1这里的1实际上是Node.DOCUMENT_POSITION_DISCONNECTED(常量)。两值按位与,如果返回的值不为0说明位置常量代表的位置关系成立。

尽管在日常的业务代码中我们很少会用到按位运算以及移位运算,但是在某些场景下利用按位运算和移位运算可以很方便的解决问题。例如,权限约束。定义某个模块有新增、修改、删除和查看的操作,如果一个用户拥有所有的操作权限,那么可以定义的权限值为‘1111’,如果都没有权限则为‘0000’,即有权限的操作位为1,无权限的操作位为0。

Attributes
	// Support: IE<8
	// Verify that getAttribute really returns attributes and not properties
	// 验证getAttribute确实返回特性而不是属性
	// (excepting IE8 booleans)
	support.attributes = assert(function (el) {
		el.className = "i";
		return !el.getAttribute("className");
	});

IE7与高版本的表现正好相反

	var el = document.createElement("fieldset");
    el.className = 'i'
    
    // chrome 下
    el.getAttribute('class') // "i"
    el.getAttribute('className') // null
    
    // IE7 下
    el.getAttribute('class') // null
    el.getAttribute('className') // "i" 

暴露接口

// EXPOSE
	var _sizzle = window.Sizzle;

    // 防止Sizzle 命名冲突,参见jquery.noConfilct()
	Sizzle.noConflict = function () {
		if (window.Sizzle === Sizzle) {
			window.Sizzle = _sizzle;
		}

		return Sizzle;
	};

	if (typeof define === "function" && define.amd) {
		define(function () { return Sizzle; });
		// Sizzle requires that there be a global window in Common-JS like environments
	} else if (typeof module !== "undefined" && module.exports) {
		module.exports = Sizzle;
	} else {
		window.Sizzle = Sizzle;
	}
	// EXPOSE

setDocument()

// Return early from calls with invalid selector or context
// 如果选择器不是 string 类型或者选择器是空字符串,
// 又或者当前上下文不是元素、document对象及iframe中的任意一种,
// 那么就认为这是一个非法的选择器或者上下文
if ( typeof selector !== "string" || !selector ||
	nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {

	return results;
}
if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
	setDocument( context );
}

在 document 中的任一 dom 对象的 ownerDocument 都是 document,而 document 本身的 ownerDocument 为空。因此,这个判断实际上是说,如果当前 context 不为空,且 context 不是 document 对象,那么执行 setDocument( context );

在了解setDocument() 函数前我们要先了解几个对象及方法

// Expose support vars for convenience
// 用于判断浏览器兼容
support = Sizzle.support = {};

/**
 * Detects XML nodes
 * 检测当前元素是html对象还是xml对象
 * @param {Element|Object} elem An element or a document
 * @returns {Boolean} True iff elem is a non-HTML XML node
 */
isXML = Sizzle.isXML = function( elem ) {
	// documentElement is verified for cases where it doesn't yet exist
	// (such as loading iframes in IE - #4833)
	var documentElement = elem && (elem.ownerDocument || elem).documentElement;
	// document 对象的 documentElement 为页面里面的<html>...</html>节点
	return documentElement ? documentElement.nodeName !== "HTML" : false;
};
disabledAncestor = addCombinator(
	function (elem) {
		return elem.disabled === true && ("form" in elem || "label" in elem);
	}, {
		dir: "parentNode",
		next: "legend"
	}
);

function addCombinator(matcher, combinator, base) {
	var dir = combinator.dir,
		skip = combinator.next,
		key = skip || dir,
		checkNonElements = base && key === "parentNode",
		doneName = done++;

	return combinator.first ?
		// Check against closest ancestor/preceding element
		function (elem, context, xml) {
			while ((elem = elem[dir])) {
				if (elem.nodeType === 1 || checkNonElements) {
					return matcher(elem, context, xml);
				}
			}
			return false;
		} :

		// Check against all ancestor/preceding elements
		function (elem, context, xml) {
			var oldCache, uniqueCache, outerCache,
				newCache = [dirruns, doneName];

			// We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching
			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] = {});

						// Support: IE <9 only
						// Defend against cloned attroperties (jQuery gh-1709)
						uniqueCache = outerCache[elem.uniqueID] || (outerCache[elem.uniqueID] = {});

						if (skip && skip === elem.nodeName.toLowerCase()) {
							elem = elem[dir] || elem;
						} else if ((oldCache = uniqueCache[key]) &&
							oldCache[0] === dirruns && oldCache[1] === doneName) {

							// Assign to newCache so results back-propagate to previous elements
							return (newCache[2] = oldCache[2]);
						} else {
							// Reuse newcache so results back-propagate to previous elements
							uniqueCache[key] = newCache;

							// A match means we're done; a fail means we have to keep checking
							if ((newCache[2] = matcher(elem, context, xml))) {
								return true;
							}
						}
					}
				}
			}
			return false;
		};
}

这里有一个概念,表单关联元素(form-associated element)

let dir = 'parentNode'
while ((elem = elem[dir])) {
	if (elem.nodeType === 1 ) {
		// ...
	}
}

可以用来遍历查找元素的所有父元素

document.defaultView 为了兼容低版本的火狐浏览器,获取computedStyle()

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值