解密jQuery內核 DOM操作的核心buildFragment


文檔碎片是什么

http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-B63ED1A3

DocumentFragment is a "lightweight" or "minimal" Document object. It is very common to want to be able to extract a portion of a document's tree or to create a new fragment of a document

參考標准的描述,DocumentFragment是一個輕量級的文檔對象,能夠提取部分文檔的樹或創建一個新的文檔片段

換句話說有文檔緩存的作用

 


createDocumentFragment有什么作用

多次使用節點方法(如:appendChild)繪制頁面,每次都要刷新頁面一次。效率也就大打折扣了,而使用document_createDocumentFragment()創建一個文檔碎片,把所有的新結點附加在其上,然后把文檔碎片的內容一次性添加到document中,這也就只需要一次頁面刷新就可。

 


DocumentFragment類型

在所有節點類型中,只有DocumentFragment在文檔中沒有對應的標記。DOM規定文檔片段(documentfragment)是一種”輕量級“的文檔,可以包含和控制節點,但不會像完整的文檔那樣占用額外資源。DocumentFragment節點具有下列特征:

  • nodeType的值為11
  • nodeName的值為“#document-fragment”
  • nodeValue的值為null
  • parentNode的值為null
  • 子節點可以是Element、ProcessingInstruction、Comment、Text、CDATASection或EntityReference

雖然不能把文檔片段直接添加到文檔中,但可以將它作為一個“倉庫”來使用,即可以在里面保存將來可能會添加到文檔中的節點。要創建文檔片段,可以使用document.createDocumentFragment()方法,如下所示:

var fragment = document.createDocumentFragment();

文檔片段繼承了Node的所有方法,通常用於執行那些針對文檔的DOM操作。如果將文檔中的節點添加到文檔片段中,就會從文檔樹中再看到該節點。添加到文檔片段中的新節點同樣也不屬於文檔樹。可以通過appendChild()或insertBefore()將文檔片段中內容添加到文檔中。在將文檔片段作為參數傳遞給這兩個方法時,實際上只會將文檔片段的所有子節點添加到相應的位置上;文檔片段本身永遠不會稱為文檔樹的一部分

http://www.w3cmm.com/dom/documentfragment.html

 


createElement與createDocumentFragment

createElement是創建一個新的節點,createDocumentFragment是創建一個文檔片段

DocumentFragment 接口表示文檔的一部分(或一段)。更確切地說,它表示一個或多個鄰接的 Document 節點和它們的所有子孫節點。

DocumentFragment 節點不屬於文檔樹,繼承的 parentNode 屬性總是 null。

不過它有一種特殊的行為,該行為使得它非常有用

即當請求把一個 DocumentFragment 節點插入文檔樹時,插入的不是 DocumentFragment 自身,而是它的所有子孫節點。這使得 DocumentFragment 成了有用的占位符,暫時存放那些一次插入文檔的節點。它還有利於實現文檔的剪切、復制和粘貼操作,尤其是與 Range 接口一起使用時更是如此

可以用 Document.createDocumentFragment() 方法創建新的空 DocumentFragment 節點。

也可以用 Range.extractContents() 方法Range.cloneContents() 方法 獲取包含現有文檔的片段的 DocumentFragment 節點。

除此之外

createElement創建的元素可以使用innerHTML,createDocumentFragment創建的元素使用innerHTML並不能達到預期修改文檔內容的效果,只是作為一個屬性而已。兩者的節點類型完全不同,並且createDocumentFragment創建的元素在文檔中沒有對應的標記,因此在頁面上只能用js中訪問到

createElement創建的元素可以重復操作,添加之后就算從文檔里面移除依舊歸文檔所有,可以繼續操作,但是createDocumentFragment創建的元素是一次性的,添加之后再就不能操作了

在之前domManip方法中提到的iNoClone多個節點操作需要克隆,就是因為文檔碎片的特性引起的

大體了解了,我們看看jQuery對於節點操作的時候,加強版的文檔碎片buildFragment

 


buildFragment

我們知道用文檔碎片無非就是先創建

fragment = context.createDocumentFragment(),

然后把所有需要處理的dom節點給appendChild進去

buildFragment對於文檔碎片的創建,可以看到被切分了2個部分

先看第一部分代碼

收集節點元素

我們看一個參數,包含了 字符串,$對象

var $e = $('<span>e</span>'), $x = $('<span>x</span>');
    inner.after('&nbsp;', $e, '&nbsp;', $x)

 

對應的buildFragment就需要針對傳入elems的分解可以有三部分,引入一個nodes緩存起來

jQuery對象

if ( jQuery.type( elem ) === "object" ) {
// Support: QtWebKit
// jQuery.merge because core_push.apply(_, arraylike) throws
jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );

 

文本類型

nodes.push( context.createTextNode( elem ) )

 

字符串HTML

將HTML代碼賦值給一個DIV元素的innerHTML屬性,然后取DIV元素的子元素,即可得到轉換后的DOM元素、

tmp = tmp || fragment.appendChild( context.createElement("div") );

// Deserialize a standard representation
tag = ( rtagName.exec( elem ) || ["", ""] )[ 1 ].toLowerCase();
wrap = wrapMap[ tag ] || wrapMap._default;
tmp.innerHTML = wrap[ 1 ] + elem.replace( rxhtmlTag, "<$1></$2>" ) + wrap[ 2 ];

// Descend through wrappers to the right content
j = wrap[ 0 ];
while ( j-- ) {
    tmp = tmp.lastChild;
}

// Support: QtWebKit
// jQuery.merge because core_push.apply(_, arraylike) throws
jQuery.merge( nodes, tmp.childNodes );

// Remember the top-level container
tmp = fragment.firstChild;

// Fixes #12346
// Support: Webkit, IE
tmp.textContent = "";

創建了一個臨時的tmp元素(div),這樣調用innerHTML方法,用來儲存創建的節點的內容,fragment本身只是起到一個容器的作用,這點我們要記住了

 

但是jQuery引入了一個wrapMap,一個反序列化表示

用來干嘛的?

我們知道看jQuery創建元素類型可以是任意的,可以所以可以是是a,scrpit,tr,th,option等等

inner.after('<tr><tr>');
inner.after('<div><div>');

但是在並不是所有元素的的創建都是標准的,在不同瀏覽器下還是有區別,比如表格

比如在table中插入一行一列

var table = document.getElementsByTagName('table')[0]; 
        var tr = document.createElement('tr'); 
        var td = document.createElement('td'); 
        var txt = document.createTextNode('haha'); 
        td.appendChild(txt); 
        tr.appendChild(td); 
        table.appendChild(tr);

面代碼在IE 6上是執行不成功的,大家可以試一下。在IE 8以上的瀏覽器都是好用的。

IE 6上失敗的原因就是IE 6認為tr標簽必須在tbody下面。也就是說,代碼寫成下面這樣,就所有瀏覽器都OK了。

var table = document.getElementsByTagName('table')[0]; 
        var tbody = document.createElement('tbody'); 
        var tr = document.createElement('tr'); 
        var td = document.createElement('td'); 
        var txt = document.createTextNode('haha'); 
        td.appendChild(txt); 
        tr.appendChild(td); 
        tbody.appendChild(tr); 
        table.appendChild(tbody)

 

所以如果是jQuery插入一個tr標簽,就需要在內部做這樣的處理工作了

inner.after('<tr><tr>');

wrapMap就是用來做適配的

tmp.innerHTML = wrap[ 1 ] + elem.replace( rxhtmlTag, "<$1></$2>" ) + wrap[ 2 ];

拼寫出來的規則就是

innerHTML: "<table><tbody><tr></tr><tr></tr></tbody></table>"

具體有多少類似的問題我們看看

image

因為wrapMap容器打破了原來的排列組合所以tr節點位置需要重新定位

就那面這個tr,lastChild變成了table, 所以需要根據wrap[ 0 ]找到嵌套的層數

j = wrap[ 0 ];
while ( j-- ) {
    tmp = tmp.lastChild;
}

因為fragment現在還不確定是最終的,因為node可能還有其他的節點,所以

fragment.textContent = "";

 


構建文檔碎片

while ( (elem = nodes[ i++ ]) ) {
    // #4087 - If origin and destination elements are the same, and this is
    // that element, do not do anything
    if ( selection && jQuery.inArray( elem, selection ) !== -1 ) {
        continue;
    }
    contains = jQuery.contains( elem.ownerDocument, elem );
    // Append to fragment
    tmp = getAll( fragment.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 );
            }
        }
    }
}

處理第一種情況,如果元素和目標元素是相同的

http://bugs.jquery.com/ticket/4087

遍歷每一個元素放入到文檔碎片中

fragment.appendChild( elem )

還有種情況就是寫入的是scrpit標簽了,用的很少先跳過

最終返回fragment


免責聲明!

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



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