在这一章中我们将重点分析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);
// 将赋值运算的结果返回
// line 71
booleans = "checked|selected|async|autofocus|autoplay|controls|defer|"
+"disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",
里面有一些是不常用的属性或是 html5 新增的属性:
whitespace = "[\\x20\\t\\r\\n\\f]",
\x20
为空格符\t
为制表符 (tabs)\r
为回车符 (Carriage Return)\n
为换行符 (Line Feed)\f
为换页符 (Form feed) var rcombinators = new RegExp("^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*");
该正则表达式表明jquery中标识关系的连接符有如下几种:
>
在给定的父元素下匹配所有的子元素。用以匹配元素的选择器,并且它是第一个选择器的子元素。+
匹配所有紧接在 prev 元素后的 next 元素。一个有效选择器并且紧接着第一个选择器。~
匹配 prev 元素之后的所有 siblings 元素。一个选择器,并且它作为第一个选择器的同辈。注意这里是之后的,就是说在prev元素之后的同级元素,而prev元素之前的同级元素是不会被选中的。
在给定的祖先元素下匹配所有的后代元素。 var rdescend = new RegExp(whitespace + "|>");
>
与 (空格)都表示选取父级元素的后代,不同的是
>
选取直接后代,后代的后代不会被选中;而 (空格)会选取所有的子级元素。
<form>
<label>Name:</label>
<input name="name" />
<fieldset>
<label>Newsletter:</label>
<input name="newsletter" />
</fieldset>
</form>
<input name="none" />
$("form > input")
[ <input name="name" />]
$("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&11 | 1010 | 10 |
1011 | 11 | ||
1010 | 10 | ||
7&8 | 0111 | 7 | |
1000 | 8 | ||
0000 | 0 | ||
按位或 | 10|11 | 1010 | 10 |
1011 | 11 | ||
1011 | 11 | ||
7|8 | 0111 | 7 | |
1000 | 8 | ||
1111 | 15 | ||
按位异或 | 10^11 | 1010 | 10 |
1011 | 11 | ||
0001 | 1 | ||
7^8 | 0111 | 7 | |
1000 | 8 | ||
1111 | 15 |
// 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
属性可用来区分不同类型的节点,比如 元素, 文本 和 注释。
常量 | 值 | 描述 |
---|---|---|
Node.ELEMENT_NODE | 1 | 一个 元素 节点,例如 <p> 和 <div> 。 |
Node.TEXT_NODE | 3 | Element 或者 Attr 中实际的 文字 |
Node.PROCESSING_INSTRUCTION_NODE | 7 | 一个用于XML文档的 ProcessingInstruction ,例如 <?xml-stylesheet ... ?> 声明。 |
Node.COMMENT_NODE | 8 | 一个 Comment 节点。 |
Node.DOCUMENT_NODE | 9 | 一个 Document 节点。 |
Node.DOCUMENT_TYPE_NODE | 10 | 描述文档类型的 DocumentType 节点。例如 <!DOCTYPE html> 就是用于 HTML5 的。 |
Node.DOCUMENT_FRAGMENT_NODE | 11 | 一个 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>
/**
* Mark a function for special use by Sizzle
* 用Sizzle标记一个特殊用途的函数
* @param {Function} fn The function to mark //待标记的函数
*/
function markFunction(fn) {
fn[expando] = true;
return fn;
}
该方法主要用来判断浏览器对接口是否支持
/**
* 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;
}
}
这个方法主要是为了解决跨浏览器的兼容问题,通常是和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;
}
}
该方法用来辅助元素排序,详见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对象里面定义个是否支持各种“特性”。
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; // 顺序不变
}
support.sortDetached = assert(function (el) {
// Should return 1, but returns 4 (following)
return el.compareDocumentPosition(document.createElement("fieldset")) & 1;
});
compareDocumentPosition()
方法比较两个节点,并返回描述它们在文档中位置的整数。
常量名 | 十进制值 | 含义 |
---|---|---|
DOCUMENT_POSITION_DISCONNECTED | 1 | 不在同一文档中 |
DOCUMENT_POSITION_PRECEDING | 2 | otherNode在node之前 |
DOCUMENT_POSITION_FOLLOWING | 4 | otherNode在node之后 |
DOCUMENT_POSITION_CONTAINS | 8 | otherNode包含node |
DOCUMENT_POSITION_CONTAINED_BY | 16 | otherNode被node包含 |
DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | 32 | 待定 |
参见张鑫旭写的 深入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。
// 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
// 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()