1.前言
上一篇jQuery分析(2)中了解了jQuery庫的骨架實現原理,這就好比搖滾音樂,搖滾音樂不是某種音樂他就像一個音樂盒子,里面包含了各種不同的搖滾風格(山地、朋克、鄉村、流行、硬搖、金屬、迷幻等)。那么上一篇只是大致了解了jQuery的基本形狀,從這篇文章開始會深入jQuery庫的各種函數,深入詳細的去了解他,那將值得慢慢探索,發現新的神奇好玩的東西。
2.輔助函數
在jQuery.fn.init方法里面使用到了一些jQuery的靜態函數,在這里提前統一的介紹
- jQuery.merge 合並兩個數組,將第二個參數數組合並到第一個參數數組中。
- jQuery.parseHTML 解析html字符串,第一個參數html字符串,第二個參數是產生fragment的context,第三個參數是否忽略scripts默認忽略
- isPlainObject 判斷一個參數是否為javascript對象即{}
- jQuery.isFunction 判斷一個參數是否為函數
- jQuery.makeArray 合並數組(內部使用)
3.jQuery.fn.init 函數概括👻
下面圖是jQeury的構造函數參數即$()調用的參數種類集合圖
下面的代碼是可能處理各種參數的方式,多余的代碼我已刪除掉,下面代碼清晰看到selector無非就是三種類型:1、字符串 2、DOMElement 3、函數。下面將會就這三種類型進行詳細深入的分析他們的實現原理,這將需要一步一步來理解,首先腦子里要清晰每一步做了什么為什么這樣做,這樣一步一步下來才能更好的去理解jQuery的寫法。
// 匹配html標簽寫法和id選擇器
// 第一個分組是 <div> 中的div,第二個分組是#id中的id
rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,
jQuery.fn.init = function(selector, context, root) {
var match, elem;
// 沒有selector將會返回當前this,以便創建空的jQuery對象在后面會用到。
if (!selector) {
return this;
}
// 獲得初始化文檔的jQuery對象
root = root || rootjQuery;
// 處理參數為字符串參數
if (typeof selector === "string") {
// 處理參數為DOM節點
} else if (selector.nodeType) {
// 處理參數為function
} else if (jQuery.isFunction(selector)) {
}
// 處理參數為NodeLists
return jQuery.makeArray(selector, this);
};
// init函數繼承jQuery
init.prototype = jQuery.fn;
// 初始化document為jQuery對象
rootjQuery = jQuery( document );
4.參數為字符串類型分解🐢
因為這個jQuery.fn.init函數代碼很多所以單獨的參數類型會把他的代碼單獨提出來分解,下面看提出來的參數為字符串的代碼。
對於字符串參數的幾種調用方法參見上面腦圖
// 匹配html標簽寫法和id選擇器
// 第一個分組是 <div> 中的div,第二個分組是#id中的id
rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,
// 參數為字符串處理方式
if (typeof selector === "string") {
if (selector.charAt(0) === "<" &&
selector.charAt(selector.length - 1) === ">" &&
selector.length >= 3) {
// 字符串是單標簽的DOM格式直接創建正則匹配的格式以跳過正則匹配以節約性能
match = [null, selector, null];
} else {
// 匹配到DOM字符串 或者ID選擇器
match = rquickExpr.exec(selector);
}
// 如果selector是一個html字符串或者是一個ID選擇器
if (match && (match[1] || !context)) {
// html字符串解析
if (match[1]) {
context = context instanceof jQuery ? context[0] : context;
// 解析html(單標簽或多標簽)
jQuery.merge(this, jQuery.parseHTML(
match[1],
context && context.nodeType ? context.ownerDocument || context : document,
true
));
// 構建html元素時傳遞了第二個參數為一個對象,那么會對對象的key和value進行解析
if (rsingleTag.test(match[1]) && jQuery.isPlainObject(context)) {
for (match in context) {
// Properties of context are called as methods if possible
if (jQuery.isFunction(this[match])) {
this[match](context[match]);
// ...and otherwise set as attributes
} else {
this.attr(match, context[match]);
}
}
}
return this;
// id選擇器處理方式
} else {
elem = document.getElementById(match[2]);
// Check parentNode to catch when Blackberry 4.6 returns
// nodes that are no longer in the document #6963
if (elem && elem.parentNode) {
// Handle the case where IE and Opera return items
// by name instead of ID
if (elem.id !== match[2]) {
return rootjQuery.find(selector);
}
// Otherwise, we inject the element directly into the jQuery object
this.length = 1;
this[0] = elem;
}
this.context = document;
this.selector = selector;
return this;
}
// 其他選擇器(class、element、attr)等
} else if (!context || context.jquery) {
return (context || root).find(selector);
// 其他選擇器(class、element、attr)等且有context(一個DOMElement)
} else {
return this.constructor(context).find(selector);
}
}
- 4-1.字符串處理
- 4-1-1.selector為單標簽的html會跳過正則表達式匹配,直接構造出一個正則表達結果結果放置match變量中。代碼
- 4-1-2.匹配selector是否為為多標簽或者ID選擇器結果放置match變量中代碼
- 4-1-3.selector是一個html字符串會對他們進行解析(單標簽/多標簽)代碼,如果還傳遞了額外的props屬性也會在解析和創建完DOM節點后附加到這個DOM上代碼
- 4-1-4.selector為ID選擇器則直接調用
document.getElementById
進行ID選擇元素,把選擇到的元素放入this[0]中,隨后修正length、context、selector即可代碼 - 4-1-5.selector為其他選擇器(class、element、attr等)並且沒有給定第二個參數(context)或者第二個參數(context)是一個jquery對象那么會調用
(context || root).find(selector)
進行查找元素代碼 - 4-1-6.selector為其他選擇器(class、element、attr等)並且第二個參數(context)為一個DOMElement會先構建context為一個jQuery對象再利用這個對象進行.find(selector)查找代碼
5.字符串解析流程中涉及的函數分析
4-1把所有的字符串解析流程羅列了出來,在這個流程中所涉及了一些關鍵的函數在這里給拆分解析一下。
- 5-1.jQuery.parseHTML解析html字符串為DOM節點
// data: string of html
// context (optional): If specified, the fragment will be created in this context,
// defaults to document
// keepScripts (optional): If true, will include scripts passed in the html string
jQuery.parseHTML = function( data, context, keepScripts ) {
if ( !data || typeof data !== "string" ) {
return null;
}
// 修正參數,只有2個參數情況下忽略context
if ( typeof context === "boolean" ) {
keepScripts = context;
context = false;
}
// 修正context默認為document
context = context || document;
var parsed = rsingleTag.exec( data ),
scripts = !keepScripts && [];
// Single tag
if ( parsed ) {
return [ context.createElement( parsed[ 1 ] ) ];
}
parsed = buildFragment( [ data ], context, scripts );
// 移除已經執行過的腳本
if ( scripts && scripts.length ) {
jQuery( scripts ).remove();
}
// 合並並返回標dom集合的數組
return jQuery.merge( [], parsed.childNodes );
};
這個函數一共有三個參數,參數一是一個html字符串,參數二是創建fragment的context,參數三是表示是否保留scripts腳本默認為false
// 單標簽
var parsed = rsingleTag.exec( data ),
scripts = !keepScripts && [];
// Single tag
if ( parsed ) {
return [ context.createElement( parsed[ 1 ] ) ];
}
如果data參數為一個單標簽html字符串("
// 多標簽
parsed = buildFragment( [ data ], context, scripts );
如果data參數為一個多標簽html字符串("
最后返回一個數組元素,里面是所有解析好的DOM元素
- 5-2.buildFragment 創建文檔片
其實在整個selector為字符串參數的代碼處理中,buildFragment應該還是算代碼比較多的了,其他那些class、element、attr等都是Sizzle選擇器引擎搞定了,所以buildFragment還是一個比較有看頭的函數,其中文檔碎片技術和必須要外包裹標簽的創建方法其實我們平時編碼時也會經常使用,可以借鑒一二。
/*
buildFragment 重要的參數是前面3個
elems 一個待轉換的html字符串
context 轉換上下文
scripts 是否忽略script標簽
*/
function buildFragment(elems, context, scripts, selection, ignored) {
var j, elem, contains,
tmp, tag, tbody, wrap,
l = elems.length,
// Ensure a safe fragment
// 創建文檔碎片
safe = createSafeFragment(context),
nodes = [],
i = 0;
for (; i < l; i++) {
elem = elems[i];
if (elem || elem === 0) {
// Add nodes directly
// 如果在elems中的某個數組元素是對象直接添加到nodes
if (jQuery.type(elem) === "object") {
jQuery.merge(nodes, elem.nodeType ? [elem] : elem);
// Convert non-html into a text node
// 轉換非html的字符串為文本節點
} else if (!rhtml.test(elem)) {
nodes.push(context.createTextNode(elem));
// Convert html into DOM nodes
} else {
//給文檔碎片創建一個元素,用來裝接下來我們需要的html字符串
tmp = tmp || safe.appendChild(context.createElement("div"));
// Deserialize a standard representation
// 取得標簽名稱,並轉為小寫
tag = (rtagName.exec(elem) || ["", ""])[1].toLowerCase();
// 需要其他元素包裹的標簽
wrap = wrapMap[tag] || wrapMap._default;
//把我們的html字符串放入剛剛創建文檔碎片的div中形成dom元素
// 如果需要包裹元素則把html字符串進行包裹 <td>123</td> => <table><tbody><tr><td>abc</td></tr></tbody></table>
tmp.innerHTML = wrap[1] + jQuery.htmlPrefilter(elem) + wrap[2];
// Descend through wrappers to the right content
// 取得剛剛創建的元素
j = wrap[0];
while (j--) {
tmp = tmp.lastChild;
}
// Manually add leading whitespace removed by IE
if (!support.leadingWhitespace && rleadingWhitespace.test(elem)) {
nodes.push(context.createTextNode(rleadingWhitespace.exec(elem)[0]));
}
// Remove IE's autoinserted <tbody> from table fragments
if (!support.tbody) {
// String was a <table>, *may* have spurious <tbody>
elem = tag === "table" && !rtbody.test(elem) ?
tmp.firstChild :
// String was a bare <thead> or <tfoot>
wrap[1] === "<table>" && !rtbody.test(elem) ?
tmp :
0;
j = elem && elem.childNodes.length;
while (j--) {
if (jQuery.nodeName((tbody = elem.childNodes[j]), "tbody") &&
!tbody.childNodes.length) {
elem.removeChild(tbody);
}
}
}
// 把創建好的dom節點也就是tmp的子節點合並到nodes中
jQuery.merge(nodes, tmp.childNodes);
// Fix #12392 for WebKit and IE > 9
tmp.textContent = "";
// Fix #12392 for oldIE
while (tmp.firstChild) {
tmp.removeChild(tmp.firstChild);
}
// Remember the top-level container for proper cleanup
tmp = safe.lastChild;
}
}
}
// Fix #11356: Clear elements from fragment
// 清除文檔碎片中的元素
if (tmp) {
safe.removeChild(tmp);
}
// Reset defaultChecked for any radios and checkboxes
// about to be appended to the DOM in IE 6/7 (#8060)
if (!support.appendChecked) {
jQuery.grep(getAll(nodes, "input"), fixDefaultChecked);
}
i = 0;
while ((elem = nodes[i++])) {
// Skip elements already in the context collection (trac-4087)
if (selection && jQuery.inArray(elem, selection) > -1) {
if (ignored) {
ignored.push(elem);
}
continue;
}
// 元素是否已經包含在document中
contains = jQuery.contains(elem.ownerDocument, elem);
// Append to fragment
// 將創建好的元素再次添加到文檔碎片中,並取得scirpt標簽
tmp = getAll(safe.appendChild(elem), "script");
// Preserve script evaluation history
if (contains) {
setGlobalEval(tmp);
}
// Capture executables
// 收集要執行的腳本
if (scripts) {
j = 0;
while ((elem = tmp[j++])) {
if (rscriptType.test(elem.type || "")) {
scripts.push(elem);
}
}
}
}
tmp = null;
// 返回創建好的文檔碎片
return safe;
}
關於buildFragment函數里面還有一些兼容性的解決方案還沒分析到,后面再做吧。至此關於jQuery.fn.init函數的構造流程也就分析完畢。