解密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(' ', $e, ' ', $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>"
具體有多少類似的問題我們看看
因為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