zoukankan      html  css  js  c++  java
  • jQuery1.11源码分析(3)-----Sizzle源码中的浏览器兼容性检测和处理[原创]

    上一章讲了正则表达式,这一章继续我们的前菜,浏览器兼容性处理。

    先介绍一个简单的沙盒测试函数。

    /**
     * Support testing using an element
     * @param {Function} fn Passed the created div and expects a boolean result
     */
    //特性检测的一个技巧,造一个div,最后将其删掉,提供一种简单的沙盒(如果该div不加入到DOM树上,则部分属性如currentStyle会没有)
    function assert( fn ) {
    	var div = document.createElement("div");
    
    	try {
    		return !!fn( div );
    	} catch (e) {
    		return false;
    	} finally {
    		// Remove from its parent by default
    		if ( div.parentNode ) {
                //为了不造成影响,最后如果该div还在DOM树上,就将其移除掉
    			div.parentNode.removeChild( div );
    		}
    		// release memory in IE
            // IE下对于不在DOM树里的DOM节点必须手动置为null,关于IE内存泄露的文章很多,不再赘述
    		div = null;
    	}
    }
    

    嗯,说明如注释,接下来我们先处理push函数,有些浏览器Nodelist是不能使用push的,我们先检测一下在当前浏览器下push能不能支持Nodelist,不能的话则把push换成自己的方法,使push能够支持Nodelist

    // Optimize for push.apply( _, NodeList )
    //居然还可以对push进行优化,看看push支不支持Nodelist
    //因为Nodelist不是数组,所以在某些浏览器下没有Push方法,需要人工造一个支持nodelist的push方法。
    try {
    	push.apply(
    		(arr = slice.call( preferredDoc.childNodes )),
    		preferredDoc.childNodes
    	);
    	// Support: Android < 4.0
    	// Detect silently failing push.apply
        //防止安卓4.0以下版本静默失败(失败了但不报错)。
    	arr[ preferredDoc.childNodes.length ].nodeType;
    } catch ( e ) {
        //如果arr有值,即arr.length > 0,说明上述push方法是有效的,使用原生API,否则换成自己的方法
    	push = { apply: arr.length ?
    		// Leverage slice if possible
    		function( target, els ) {
                //这里一定要用apply,否则会把整个els给Push进去而不是拆成一个个push
    			push_native.apply( target, slice.call(els) );
    		} :
    
    		// Support: IE<9
    		// Otherwise append directly
            //这里不明白为什么。。
            //因为在IE下对Nodelist执行slice会报错找不到JScript对象,所以arr.length为0
            //http://www.jb51.net/article/24182.htm
    		function( target, els ) {
    			var j = target.length,
    				i = 0;
    			// Can't trust NodeList.length
                //为什么IE8下不能相信长度?
    			while ( (target[j++] = els[i++]) ) {}
    			target.length = j - 1;
    		}
    	};
    }
    

    说明如注释,然后再来三发对于html元素向DOM元素转换时,部分属性会遇到的bug以及处理

    // Support: IE<8
    // Prevent attribute/property "interpolation"
    // http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
    //在IE<8下会自动串改部分属性,比如下面的href会改成当前地址+#,大家可以试试。
    //出现这种情况后,当我们要取某些属性时,交给代理函数进行处理,比如Expr.attrHandle
    if ( !assert(function( div ) {
    	div.innerHTML = "<a href='#'></a>";
    	return div.firstChild.getAttribute("href") === "#" ;
    }) ) {
    	addHandle( "type|href|height|width", function( elem, name, isXML ) {
    		if ( !isXML ) {
                //getAttribute我搜了一下。。没有第二个参数,还没翻标准。。暂且认为是错的。
    //我靠。。还真有第二个参数。。http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); } }); } // Support: IE<9 // Use defaultValue in place of getAttribute("value") if ( !support.attributes || !assert(function( div ) { div.innerHTML = "<input/>"; div.firstChild.setAttribute( "value", "" ); return div.firstChild.getAttribute( "value" ) === ""; }) ) { addHandle( "value", function( elem, name, isXML ) { if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { return elem.defaultValue; } }); } // Support: IE<9 // Use getAttributeNode to fetch booleans when getAttribute lies //使用getAttribute获取那些非attrName=attrValue形式的attr属性时,会出错,所以换成使用getAttributeNode,然后判断获得的Attr节点的specified属性,看看是否指定。 if ( !assert(function( div ) { return div.getAttribute("disabled") == null; }) ) { addHandle( booleans, function( elem, name, isXML ) { var val; if ( !isXML ) { return elem[ name ] === true ? name.toLowerCase() : (val = elem.getAttributeNode( name )) && val.specified ? val.value : null; } }); }

    说明如注释,接下来我们先声明一个对象,这个对象类似于配置文件(config)的作用,嗯。。配置文件通常很长

    Expr = Sizzle.selectors = {
    
    	// Can be adjusted by the user
        //缓存的长度
    	cacheLength: 50,
        //用来标识奇葩函数
    	createPseudo: markFunction,
        //存放用来匹配的表达式
    	match: matchExpr,
        //前面说过的,存放提取部分特殊属性的handle
    	attrHandle: {},
        //查找过程所用的函数就存在这,后面会进行声明
    	find: {},
        //前面说个token中有一种类型是表示两个元素的关系,这个关系处理函数用relative保存起来,first用来表示是否是查找到的第一个元素。
        //例如假设tokens[i]是{type:'>',value:'>'},调用body[Expr.relative[tokens[i].type][dir]],
    	relative: {
    		">": { dir: "parentNode", first: true },
    		" ": { dir: "parentNode" },
    		"+": { dir: "previousSibling", first: true },
    		"~": { dir: "previousSibling" }
    	},
    

    在Expr里还有三个部分:preFilter(用来对捕获组进行预处理),filter(返回一个个matcher,最后将多个matcher编译成一个),pseudo(其实就是filter的一种),后续可能留出篇幅来进行讲解。

    接下来我们休息一下,再一口气啃掉一个超长的初始化函数。这个初始化的主要作用是从健壮性的考虑出发,设置一下文档节点,检查一下是不是HTML,检查一下各个原生API是不是好用,检查一下querySelectorAll是不是好用,初始化查找过程用的函数(比如Expr.find['ID']);

    /**
     * Sets document-related variables once based on the current document
     * @param {Element|Object} [doc] An element or document object to use to set the document
     * @returns {Object} Returns the current document
     */
    //基于当前文档节点设置一些文档相关的内容,如支持性什么的。
    setDocument = Sizzle.setDocument = function( node ) {
        console.log('setDocument in');
    	var hasCompare,
    		doc = node ? node.ownerDocument || node : preferredDoc,         //获得文档节点的一种方式,这样写的好处是保持健壮性
    		parent = doc.defaultView;
    
    	// If no document and documentElement is available, return
        //nodeType不等于9说明不是文档节点
        //做这么多判断。。嗯。。还是为了健壮性
    	if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
    		return document;
    	}
    
    	// Set our document
    	document = doc;
    	docElem = doc.documentElement;
    
    	// Support tests
    	documentIsHTML = !isXML( doc );
    
    	// Support: IE>8
    	// If iframe document is assigned to "document" variable and if iframe has been reloaded,
    	// IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936
    	// IE6-8 do not support the defaultView property so parent will be undefined
        //这里是一个jQuery的bug修复
    	if ( parent && parent !== parent.top ) {
    		// IE11 does not have attachEvent, so all must suffer
    		if ( parent.addEventListener ) {
    			parent.addEventListener( "unload", function() {
    				setDocument();
    			}, false );
    		} else if ( parent.attachEvent ) {
    			parent.attachEvent( "onunload", function() {
    				setDocument();
    			});
    		}
    	}
    
    	/* Attributes
    	---------------------------------------------------------------------- */
    
    	// Support: IE<8
    	// Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans)
        //如果这里检测通过的话,后面获取属性值用的就是getAttribute这个API,否则用的就是getAttributeNode
    	support.attributes = assert(function( div ) {
    		div.className = "i";
    		return !div.getAttribute("className");
    	});
    
    	/* getElement(s)By*
    	---------------------------------------------------------------------- */
    
    	// Check if getElementsByTagName("*") returns only elements
        //检查getElementsByTagName是否会包含注释节点,最好不要包含。
    	support.getElementsByTagName = assert(function( div ) {
    		div.appendChild( doc.createComment("") );
    		return !div.getElementsByTagName("*").length;
    	});
    
    	// Check if getElementsByClassName can be trusted
        //?????细节
    	support.getElementsByClassName = rnative.test( doc.getElementsByClassName ) && assert(function( div ) {
    		div.innerHTML = "<div class='a'></div><div class='a i'></div>";
    
    		// Support: Safari<4
    		// Catch class over-caching
    		div.firstChild.className = "i";
    		// Support: Opera<10
    		// Catch gEBCN failure to find non-leading classes
            //Opera<10 的时候当一个元素有多个class的时候,获得第二个会出错。
    		return div.getElementsByClassName("i").length === 2;
    	});
    	// Support: IE<10
    	// Check if getElementById returns elements by name
    	// The broken getElementById methods don't pick up programatically-set names,
    	// so use a roundabout getElementsByName test
        //????所以迂回使用getElementsByName来检测?不明原理
    	support.getById = assert(function( div ) {
    		docElem.appendChild( div ).id = expando;
    		return !doc.getElementsByName || !doc.getElementsByName( expando ).length;
    	});
        //Expr里的函数分为查找和过滤两种功能类型。
        //接下来分别进行这两种函数功能性的检测和支持
    	// ID find and filter
    	if ( support.getById ) {
    
    		Expr.find["ID"] = function( id, context ) {
                //??????为什么要转换成字符串形式的undefined
    			if ( typeof context.getElementById !== strundefined && documentIsHTML ) {
    				var m = context.getElementById( id );
    				// Check parentNode to catch when Blackberry 4.6 returns
    				// nodes that are no longer in the document #6963
                    //!!!!在黑莓4.6的浏览器中会返回那些不在DOM树中的节点,所以通过该节点是否有父节点来判断该节点是否在DOM树中,bug修正,并且转换为数组形式。
    				return m && m.parentNode ? [m] : [];
    			}
    		};
            //这里返回一个后面用来做matcher的函数
    		Expr.filter["ID"] = function( id ) {
                //对id做编码转换,这是一种闭包类型。
    			var attrId = id.replace( runescape, funescape );
    			return function( elem ) {
    				return elem.getAttribute("id") === attrId;
    			};
    		};
    	} else {
    		// Support: IE6/7
    		// getElementById is not reliable as a find shortcut
            //???getElementById不可靠??
            //!!!!删掉以后,Sizzle就不再通过ID来获取节点,获取属性的方式也由getAttribute变为getAttributeNode
    		delete Expr.find["ID"];
    
    		Expr.filter["ID"] =  function( id ) {
    			var attrId = id.replace( runescape, funescape );
    			return function( elem ) {
                    //每个DOM元素下有隐含的属性节点,通过查看其属性节点的方式来过滤
    				var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id");
    				return node && node.value === attrId;
    			};
    		};
    	}
    
    	// Tag
    	Expr.find["TAG"] = support.getElementsByTagName ?
    		function( tag, context ) {
                console.log('find tag begin');
    			if ( typeof context.getElementsByTagName !== strundefined ) {
    				return context.getElementsByTagName( tag );
    			}
    
    		} :
            //否则采用过滤掉注释节点的写法。
    		function( tag, context ) {
                console.log('find tag begin');
    			var elem,
    				tmp = [],
    				i = 0,
    				results = context.getElementsByTagName( tag );
    
    			// Filter out possible comments
                //必须过滤注释节点
    			if ( tag === "*" ) {
                    //赋值判断写法。
    				while ( (elem = results[i++]) ) {
    					if ( elem.nodeType === 1 ) {
    						tmp.push( elem );
    					}
    				}
    
    				return tmp;
    			}
    			return results;
    		};
    
    	// Class
    	Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
            console.log('find class begin');
    		if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) {
    			return context.getElementsByClassName( className );
    		}
    	};
    
    
    	/* QSA/matchesSelector
    	---------------------------------------------------------------------- */
    
    	// QSA and matchesSelector support
    
    	// matchesSelector(:active) reports false when true (IE9/Opera 11.5)
    	rbuggyMatches = [];
    
    	// qSa(:focus) reports false when true (Chrome 21)
    	// We allow this because of a bug in IE8/9 that throws an error
    	// whenever `document.activeElement` is accessed on an iframe
    	// So, we allow :focus to pass through QSA all the time to avoid the IE error
    	// See http://bugs.jquery.com/ticket/13378
        //用来存放有bug的QSA字符串,最后用|连接起来当作正则表达式,用来检测选择符是否有bug
    	rbuggyQSA = [];
    
    	if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) {
    		// Build QSA regex
    		// Regex strategy adopted from Diego Perini
            //这两个assert没有返回值,主要是把有bug的QSA字符串检测出来。
    		assert(function( div ) {
    			// Select is set to empty string on purpose
    			// This is to test IE's treatment of not explicitly
    			// setting a boolean content attribute,
    			// since its presence should be enough
    			// http://bugs.jquery.com/ticket/12359
                //这个bug检测很简练
    			div.innerHTML = "<select>
    <option selected="selected"></option>
    
    
    
    
    </select>";
    
    			// Support: IE8, Opera 10-12
    			// Nothing should be selected when empty strings follow ^= or $= or *=
                //!!!空白字符串不应该跟在 ^=、$=、*=这样的字符后面,否则逻辑上是走不通的,你想想啊,^=''的意思是匹配开头为空字符的字符串。。哪个字符串开头是空字符?,剩下同理
    			if ( div.querySelectorAll("[t^='']").length ) {
    				rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|"")" );
    			}
    
    			// Support: IE8
    			// Boolean attributes and "value" are not treated correctly
                //!!!IE8中的QSA不能正确识别非key=value形式的属性选择符
    			if ( !div.querySelectorAll("[selected]").length ) {
    				rbuggyQSA.push( "\[" + whitespace + "*(?:value|" + booleans + ")" );
    			}
    
    			// Webkit/Opera - :checked should return selected option elements
    			// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
    			// IE8 throws error here and will not see later tests
                //!!!!:checked伪类选择器理论应该返回被选择的option元素,但IE8下有bug。。
    			if ( !div.querySelectorAll(":checked").length ) {
    				rbuggyQSA.push(":checked");
    			}
    		});
    
    		assert(function( div ) {
    			// Support: Windows 8 Native Apps
    			// The type and name attributes are restricted during .innerHTML assignment
                //????卧槽。。win8不能直接用innerHTML来创建type和name属性?
    			var input = doc.createElement("input");
    			input.setAttribute( "type", "hidden" );
    			div.appendChild( input ).setAttribute( "name", "D" );
    
    			// Support: IE8
    			// Enforce case-sensitivity of name attribute
                //!!!!增强对name大小写的敏感性,如果大小写不敏感,则不能使用带有name=.的属性选择符
    			if ( div.querySelectorAll("[name=d]").length ) {
    				rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" );
    			}
    
    			// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
    			// IE8 throws error here and will not see later tests
                //!!!!理论上hidden的元素还应该能用:enabled伪类选择符选择到的,但IE8和FF3.5下有bug
    			if ( !div.querySelectorAll(":enabled").length ) {
    				rbuggyQSA.push( ":enabled", ":disabled" );
    			}
    
    			// Opera 10-11 does not throw on post-comma invalid pseudos
                //?????
    			div.querySelectorAll("*,:x");
    			rbuggyQSA.push(",.*:");
    		});
    	}
    
    	if ( (support.matchesSelector = rnative.test( (matches = docElem.webkitMatchesSelector ||
    		docElem.mozMatchesSelector ||
    		docElem.oMatchesSelector ||
    		docElem.msMatchesSelector) )) ) {
    
    		assert(function( div ) {
    			// Check to see if it's possible to do matchesSelector
    			// on a disconnected node (IE 9)
                //!!!!检测是否matchesSelector会匹配到没有连接的节点
    			support.disconnectedMatch = matches.call( div, "div" );
    
    			// This should fail with an exception
    			// Gecko does not error, returns false instead
                //?????
    			matches.call( div, "[s!='']:x" );
    			rbuggyMatches.push( "!=", pseudos );
    		});
    	}
    
    	rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );
    	rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );
    
    	/* Contains
    	---------------------------------------------------------------------- */
    	hasCompare = rnative.test( docElem.compareDocumentPosition );
    
    	// Element contains another
    	// Purposefully does not implement inclusive descendent
    	// As in, an element does not contain itself
        //这里和懒函数的方式异曲同工,如果有原生compareDocumentPosition或contains
    	contains = hasCompare || rnative.test( docElem.contains ) ?
    		function( a, b ) {
                console.log('contains begin');
    			var adown = a.nodeType === 9 ? a.documentElement : a,
    				bup = b && b.parentNode;
    			return a === bup || !!( bup && bup.nodeType === 1 && (
    				adown.contains ?
    					adown.contains( bup ) :
                        //这里&上16仅仅是判断a是否包含bup,b如果是a本身,则两者应该不算包含关系
                        //http://www.2cto.com/kf/201301/181075.html
    					a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
    			));
    		} :
            //否则就用土办法不断遍历DOM树
    		function( a, b ) {
    			if ( b ) {
    				while ( (b = b.parentNode) ) {
    					if ( b === a ) {
    						return true;
    					}
    				}
    			}
    			return false;
    		};
    
    	/* Sorting
    	---------------------------------------------------------------------- */
    
    	// Document order sorting
        //用来传给sort函数的排序方法
    	sortOrder = hasCompare ?
    	function( a, b ) {
    		// Flag for duplicate removal
    		if ( a === b ) {
    			hasDuplicate = true;
    			return 0;
    		}
    
    		// Sort on method existence if only one input has compareDocumentPosition
            //!undefined === true
            //!function (){} === false
            //if(-1){console.log(1);} -->1
            //用以检查是否两个输入都有该API
    		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 === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {
    				return -1;
    			}
    			if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {
    				return 1;
    			}
    
    			// Maintain original order
    			return sortInput ?
                    //最后这里处理了给字符串调用sort的情况
                    //后面有一句代码
                    //support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
    				( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
    				0;
    		}
    
    		return compare & 4 ? -1 : 1;
    	} :
        //如果没有contains这样的原生API使用
    	function( a, b ) {
    		// Exit early if the nodes are identical
    		if ( a === b ) {
    			hasDuplicate = true;
    			return 0;
    		}
    
    		var cur,
    			i = 0,
    			aup = a.parentNode,
    			bup = b.parentNode,
    			ap = [ a ],
    			bp = [ b ];
    
    		// Parentless nodes are either documents or disconnected
            //只要其中有一个元素没有父元素
    		if ( !aup || !bup ) {
    			return a === doc ? -1 :
    				b === doc ? 1 :
    				aup ? -1 :
    				bup ? 1 :
    				sortInput ?
    				( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
    				0;
    
    		// If the nodes are siblings, we can do a quick check
            //或者两个元素的父元素是兄弟,就可以立马判定
    		} else if ( aup === bup ) {
    			return siblingCheck( a, b );
    		}
    
    		// Otherwise we need full lists of their ancestors for comparison
            //否则遍历出所有祖先路径,然后一一对比,直到找到分歧点,再比较分歧点的序数即可
    		cur = a;
    		while ( (cur = cur.parentNode) ) {
    			ap.unshift( cur );
    		}
    		cur = b;
    		while ( (cur = cur.parentNode) ) {
    			bp.unshift( cur );
    		}
    
    		// Walk down the tree looking for a discrepancy
    		while ( ap[i] === bp[i] ) {
    			i++;
    		}
    
    		return i ?
    			// Do a sibling check if the nodes have a common ancestor
    			siblingCheck( ap[i], bp[i] ) :
    
    			// Otherwise nodes in our document sort first
    			ap[i] === preferredDoc ? -1 :
    			bp[i] === preferredDoc ? 1 :
    			0;
    	};
        //最后返回文档节点
    	return doc;
    };
    

    上面这个初始化函数实在太长。。再调用一下就好。

    // Initialize against the default document
    setDocument();
    
  • 相关阅读:
    38丨WebSocket:沙盒里的TCP
    Jmeter安装与介绍(一)
    37丨CDN:加速我们的网络服务
    爬虫笔记:xpath和lxml(十二)
    爬虫笔记:Selenium(十一)
    36丨WAF:保护我们的网络服务
    35丨OpenResty:更灵活的Web服务器
    爬虫笔记:抓取qq群成员的头像和昵称生成词云(十)
    Python全栈工程师 (类变量、方法、继承、覆盖)
    Python全栈工程师(面向对象)
  • 原文地址:https://www.cnblogs.com/suprise/p/3624339.html
Copyright © 2011-2022 走看看