作者:nuysoft/高雲 QQ:47214707 Email:nuysoft@gmail.com
聲明:本文為原創文章,如需轉載,請注明來源並保留原文鏈接。
前記:
基於 jQuery 1.7.1 編寫;之前的系列文章以“貼源碼注釋”的方式進行講解,注釋並不適合做大段的描述和排版;本節將嘗試 錨點+按塊分析+流程圖 的方式,希望這樣能增加更詳細的描述,方便閱讀理解和加深記憶。
核心函數 .domManip()
概述
.domManip()是jQuery DOM操作的核心函數,為以下DOM操作方法提供支持:
append/appendTo prepend/prependTo before/insertBefore after/insertAfter
.domManip()做了兩部分工作:
1. 將args轉換為DOM元素,並放在一個文檔碎片中,調用jQuery.buildFragment和jQuery.clean實現
2. 執行callback,將DOM元素作為參數傳入,由callback執行實際的插入操作
關於insertAdjacentXXX
在很多分析jQuery DOM操作的資料里都提到了insertAdjacentXXX,即insertAdjacentElement、insertAdjacentHTML、insertAdjacentText,這三個方法在指定的位置插入DOM元素、HTML代碼、文本。看看它的語法:
object.insertAdjacentElement/HTML/Text( sWhere, oElement/sText )
在DOM元素object的位置sWhere處插入指定的元素oElement/sText,sWhere指定了插入位置,可選的值有:
可選值 |
功能 |
jQuery中的等價方法 |
beforeBegin |
object之前 |
.before() |
afterBegin |
前置,作為object的第一個子元素 |
.prepend() |
beforeEnd |
追加,作為object的最后一個子元素 |
.append() |
afterEnd |
object之后 |
.after() |
.domManip()定義
1: domManip: function( args, table, callback ) {
args 待插入的DOM元素或HTML代碼
table 是否需要修正tbody,這個變量是優化的結果
callback 回調函數,執行格式為callback.call( 目標元素即上下文, 待插入文檔碎片/單個DOM元素 )
局部變量初始化
2: var results, first, fragment, parent,
3: value = args[0],
4: scripts = [];
5:
value 是第一個元素,后邊只針對args[0]進行檢測,意味着args中的元素必須是統一類型;
scripts 在jQuery.buildFragment中會用到,腳本的執行在.domManip()的最后一行代碼;jQuery.buildFragment中調用jQuery.clean時將scripts作為參數傳入;jQuery.clean如果遇到script標簽,則將script放入scripts,條件是:標簽名為script 並且 未指定type或type為text/javascript;即支持插入script標簽並執行;外聯腳本通過jQuery.ajax以同步阻塞的方式請求然后執行,內聯腳本通過jQuery.globalEval執行。
規避WebKit checked屬性
6: // We can't cloneNode fragments that contain checked, in WebKit
7: if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) {
8: return this.each(function() {
9: jQuery(this).domManip( args, table, callback, true );
10: });
11: }
12:
在WebKit中,不能克隆包含了已選中多選按鈕的文檔碎片;看看if代碼塊需要滿足的條件:
不能正確拷貝選中狀態 + 參數個數為3 + value是字符串 + 已選中的多選/單選按鈕
Chrome和Safari用的都是WebKit引擎,在Chrome下jQuery.support.checkClone是true,那么問題就在Safari中; 在each的回調函數中再次調用.domManip(),但是有4個參數(增加了最后一個true),為了使 arguments.length === 3 變為false么?
看不懂在搞什么!似乎早期的#bugid沒有出現在jQuery注釋中,不好查找原因,有待繼續研究 TODO。
支持參數為函數
13: if ( jQuery.isFunction(value) ) {
14: return this.each(function(i) {
15: var self = jQuery(this);
16: args[0] = value.call(this, i, table ? self.html() : undefined);
17: self.domManip( args, table, callback );
18: });
19: }
20:
如果value是函數,則執行函數,並用返回的結果,再次調用domManip;執行value時,如果table為true則傳入innerHTML,用來修正tbody;用value的返回值替換args[0],最后用修正過的args,迭代調用.domManip()。但是這里只處理args[0]是function的情況,如果args是function數組呢?驗證一下:
$d = $('div'), i = 0, f = function() { return ++i }; $d.append( f, f, f ); // 只添加第一個,並非我所設想的會處理所有的待插入元素 $d.append( 1, 2, 3 ); // 添加3個
轉換HTML代碼為DOM元素
21: if ( this[0] ) {
22: parent = value && value.parentNode;
23:
24: // If we're in a fragment, just use that instead of building a new one
25: if ( jQuery.support.parentNode && parent && parent.nodeType === 11 && parent.childNodes.length === this.length ) {
26: results = { fragment: parent };
27:
28: } else {
29: results = jQuery.buildFragment( args, this, scripts );
30: }
31:
32: fragment = results.fragment;
33:
34: if ( fragment.childNodes.length === 1 ) {
35: first = fragment = fragment.firstChild;
36: } else {
37: first = fragment.firstChild;
38: }
39:
第25行:如果父元素是文檔碎片DocumentFragment(nodeType === 11 ),那么不需要重新創建用現成的,否則就需要新建一個文檔碎片;關於jQuery.support.parentNode,從字面上看應該是檢測瀏覽器是否支持父元素屬性parentNode,但是在jQuery的整篇源碼中沒有關於parentNode的檢測,也就是說一直是undefined,我也很懷疑還有不支持父元素屬性parentNode的瀏覽器嗎?留給插件用么?
繼續查資料,在DocumentFragment http://www.w3school.com.cn/xmldom/dom_documentfragment.asp 中有這樣的說明:DocumentFragment 節點不屬於文檔樹,繼承的 parentNode 屬性總是 null。
所以這里應該是預留的檢測:對DocumentFragment進行檢測,因為DocumentFragment的parentNode總是null。
第29行:沒有父元素或父元素不是文檔碎片,則調用 jQuery.buildFragment 創建一個包含args的文檔碎片,jQuery.buildFragment用到了緩存,重復的創建會被緩存下來(需滿足一些條件講到jQuery.buildFragment時會詳細分析),jQuery.buildFragment返回的結構是 { fragment: fragment, cacheable: cacheable }
第34~38行:獲取第一個子元素first,first在后邊用於判斷是否需要修正tr的父元素為tbody,后邊的不需要判斷么?看來默認以第一個元素為准;如果只有一個子元素,那么可以省掉文檔碎片;這么做可以更快的插入元素,簡單測試下,為了使子元素個數檢測失效,將第34行改為:
34: if ( false && fragment.childNodes.length === 1 ) {
測試用例:
for( var i = 0; i < 10; i++ ) {
$b = $('body').html('');
console.time('fragment' + i);
for( var i = 0; i < 5000; i++ ) $b.append( '<div>' );
console.timeEnd('fragment' + i);
}
判斷和不判斷各執行10次取平均值(單位ms):
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
平均 |
|
判斷 |
456 |
817 |
1077 |
546 |
544 |
416 |
536 |
515 |
343 |
328 |
557.8 |
不判斷 |
760 |
671 |
592 |
640 |
1186 |
931 |
824 |
1028 |
885 |
842 |
835.9 |
可見如果文檔碎片中只有一個子元素,插入子元素要比插入文檔碎片稍快。
到這里准備工作完成了,即把args轉換為DOM元素(准確的說是創建包含args的文檔碎片),后邊開始執行回調函數開始實際的插入操作。(前戲可真長)
執行回調函數插入DOM元素
40: if ( first ) {
41: table = table && jQuery.nodeName( first, "tr" );
42:
43: for ( var i = 0, l = this.length, lastIndex = l - 1; i < l; i++ ) {
44: callback.call(
45: table ?
46: root(this[i], first) :
47: this[i],
48: // Make sure that we do not leak memory by inadvertently discarding
49: // the original fragment (which might have attached data) instead of
50: // using it; in addition, use the original fragment object for the last
51: // item instead of first because it can end up being emptied incorrectly
52: // in certain situations (Bug #8070).
53: // Fragments from the fragment cache must always be cloned and never used
54: // in place.
55: results.cacheable || ( l > 1 && i < lastIndex ) ?
56: jQuery.clone( fragment, true, true ) :
57: fragment
58: );
59: }
60: }
61:
第40行:如果成功的創建了DOM元素,才有必要開始插入操作
第41行:tr的父元素是tbody,table指示是否需要修正tbody
第43行:遍歷當前jQuery對象中的匹配元素,緩存this.length(可算開始干活了)
第44行:執行回調函數callback,格式為callback.call( 目標元素即上下文, 待插入文檔碎片/單個DOM元素 )
第46行:如果是tr,修正目標元素即上下文
第48~54行:翻譯原文注釋:
當無意中丟棄原始文檔碎片(碎片上可能已附加數據)而不是使用它時,確保不會泄漏內存;此外,對最后一個元素使用原始文檔碎片,而不是第一個,因為它在某些情況下會被錯誤的清空。
使用文檔碎片時,如果是 可緩存的 或 緩存命中(指從緩存中取到文檔碎片),則總是克隆。
第一段參考bug列表理解:
Bug#8070 http://bugs.jquery.com/ticket/8070
Basically a recent optimization to the clone method with this commit makes wrong assumptions about the existance of getElementsByTagName on DocumentFragments.
Bug#8052 http://bugs.jquery.com/ticket/8052#comment:4
As the variables elem and clone both can be DocumentFragments it's not safe to call getElementsByTagName on them.
Because according to the specification DocumentFragements don't implement this method.
可見是由於DocumentFragements可能沒有實現getElementsByTagName,而jQuery錯誤的假設getElementsByTagName是可用的;在Sizzle中可以看到對getElementsByTagName的檢測:typeof context.getElementsByTagName !== "undefined";這個問題在1.5rc1(1.5發行候選版本)中發現,隨后的1.5中得到修復。
第55~57行:克隆文檔碎片/單個DOM元素,克隆的條件:(可緩存的 或 緩存命中) 或者 this中有多個元素(需要多次用到fragment);我們先考慮不緩存的情況,同樣忽略l>1,因為l必然大於1否則不會進入for循環;在遍歷到最后一個元素之前,一直對fragment進行克隆,最后一個元素使用創建的fragment;這里的實現和官網API的描述正好相反(http://api.jquery.com/append/ If there is more than one target element, however, cloned copies of the inserted element will be created for each target after the first.);講到jQuery.buildFragment時,會對DocumentFragment做更多的討論。
執行腳本元素
62: if ( scripts.length ) {
63: jQuery.each( scripts, evalScript );
64: }
65: }
66:
如果腳本數組scripts的長度大於0,則執行其中的腳本;在jQuery.clean中,如果遇到script標簽則會放入腳本數組scripts中,例如:
$('div').append( '<script>alert(123);</script>' )
evalScript負責執行script元素,如果是外聯腳本(即通過src引入),用jQuery.ajax同步請求src指定的地址並自動執行;如果是內聯腳本(即寫在script標簽內),用jQuery.globalEval執行。
67: return this;
68: }
鏈式語法。
從.domManip()學到的
1. 如果遇到tr,需要處理tbody的問題
2. 如果插入多個元素時,將多個元素先插入一個文檔碎片,然后將文檔碎片一次性插入指定的元素和位置
3. 將HTML代碼轉換為DOM元素,可以將HTML代碼賦值給一個DIV元素的innerHTML屬性,然后取DIV元素的子元素,即可得到轉換后的DOM元素