上一章講了正規表達式,這一章繼續我們的前菜,浏覽器相容性處理。
先介紹一個簡單的沙盒測試函數。
/**
* 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();