[原創] jQuery源碼分析-12 DOM操作-Manipulation-核心函數.domManip()


作者:nuysoft/高雲 QQ:47214707 Email:nuysoft@gmail.com
聲明:本文為原創文章,如需轉載,請注明來源並保留原文鏈接。

jQuery源碼分析系列(持續更新)

 

前記:

基於 jQuery 1.7.1 編寫;之前的系列文章以“貼源碼注釋”的方式進行講解,注釋並不適合做大段的描述和排版;本節將嘗試 錨點+按塊分析+流程圖 的方式,希望這樣能增加更詳細的描述,方便閱讀理解和加深記憶。

 

核心函數 .domManip()

 

概述

關於insertAdjacentXXX

.domManip()定義

局部變量初始化

規避WebKit checked屬性

支持參數為函數

轉換HTML代碼為DOM元素

執行回調函數插入DOM元素

從.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執行實際的插入操作

image

 

關於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()

這些功能在jQuery中都有對應的接口。sWhere的可選值一開始給我的感覺是有些丑陋,但是細想,似乎這些候選值跟能說明插入操作的本質。簡單測試了下,IE和Chrome是支持的,Firefox不支持,但insertAdjacentXXX畢竟沒有標准化(There is no public standard that applies to this method),實現一個類似的接口也不復雜。在jQuery里,與insertAdjacentXXX類似的就是.domManip()。

 

.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元素


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM