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

时间:2022-05-04 22:31:37

文档碎片是什么

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>"

具体有多少类似的问题我们看看

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

因为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