1. 程式人生 > >解密jQuery核心 DOM操作的核心buildFragment

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

文件碎片是什麼

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()將文件片段中內容新增到文件中。在將文件片段作為引數傳遞給這兩個方法時,實際上只會將文件片段的所有子節點新增到相應的位置上;文件片段本身永遠不會稱為文件樹的一部分

createElement與createDocumentFragment

createElement是建立一個新的節點,createDocumentFragment是建立一個文件片段

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

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

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

即當請求把一個 DocumentFragment 節點插入文件樹時,插入的不是 DocumentFragment 自身,而是它的所有子孫節點。這使得 DocumentFragment 成了有用的佔位符,暫時存放那些一次插入文件的節點。它還有利於實現文件的剪下、複製和貼上操作,尤其是與 Range 介面一起使用時更是如此

可以用 Document.createDocumentFragment() 方法建立新的空 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 );
            }
        }
    }
}

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

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

fragment.appendChild( elem )

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

最終返回fragment