解密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(' ', $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 ); } } } }
处理第一种情况,如果元素和目标元素是相同的
http://bugs.jquery.com/ticket/4087
遍历每一个元素放入到文档碎片中
fragment.appendChild( elem )
还有种情况就是写入的是scrpit标签了,用的很少先跳过
最终返回fragment