jQuery.buildFragment源码分析以及在构造jQuery对象的作用
这个方法在jQuery源码中比较靠后的位置出现,主要用于两处。1是构造jQuery对象的时候使用 2.是为DOM操作提供底层支持,这也就是为什么先学习它的原因。之前的随笔已经分析过jQuery的构造函数了,也提到了有12个分支,其中有一个分支就是通过jQuery.buildFragment方法来处理的,什么情况呢?就是在处理复杂html标签的时候,例如$('<div>123</div>')这样的形式,在构造函数内部通过ret变量判断是不是简单标签,如果是就调用js的createElement方法直接创建元素如果不是呢就通过这个方法处理相关讲解可以参考字符串情况分类分析。本文主要是讨论此方法在构造jQuery对象上的作用。
了解文档片段documentFragment
Javascript中有documentFragment方法用于创建文档片段,什么是文档片段呢?就是创建出来的元素表示文档的一部分但是却不属于文档树。可以简单的理解为缓存的文档元素,通常我们会利用它来创建文档元素,我们可以利用文档片段的特性先把要插入的文档创建在文档片段中然后整体插入,相对于一个一个的插入文档元素而言性能会提高很多。这里引用一个例子:
//假如想创建十个段落,使用常规的方式可能会写出这样的代码: var i = 0 ; i < 10; i ++) { var p = document.createElement("p"); var oTxt = document.createTextNode("段落" + i); p.appendChild(oTxt); document.body.appendChild(p); } 当然,这段代码运行是没有问题,但是他调用了十次document.body.appendChild(),每次都要产生一次页面渲染。这时碎片就十分有用了: var oFragment = document.createDocumentFragment(); for(var i = 0 ; i < 10; i ++) { var p = document.createElement("p"); var oTxt = document.createTextNode("段落" + i); p.appendChild(oTxt); oFragment.appendChild(p);<br>} document.body.appendChild(oFragment);
在这段代码中,每个新的<p />元素都被添加到文档碎片中,然后这个碎片被作为参数传递给appendChild()。这里对appendChild()的调用实际上并不是把文档碎片本省追加到body元素中,而是仅仅追加碎片中的子节点,然后可以看到明显的性能提升,document.body.appenChild()一次替代十次,这意味着只需要进行一个内容渲染刷新。
实现原理
jQuery.buildFragment方法会首先创建一个文档片段,然后在调用jQuery.clean方法将结果转换为dom元素,其中为了更好了提高效率,jQuery在此方法中加入了缓存机制,如果符合缓存会在读取时知己从缓存读取设置的时候也会有一个缓存备份供下一次使用。
源码分析
jQuery.buildFragment = function( args, nodes, scripts ) { var fragment, cacheable, cacheresults, doc,
first = args[ 0 ];
...
}
首先会接受3个参数args表示的待转换为DOM元素的HTML代码它是一个数组;nodes也是一个数组用于修正创建文档片段的的文档对象;script是存放script元素的这个主要跟dom操作有关系。这样看还是觉得没什么概念这些参数哪里来的?那么就找找之前调用方法的地方:
} else { ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; }
在判断不是简单标签的时候就把[ match[1] ], [ doc ]这个两个东西传递了进来,现在知道为什么前两个参数都是数组了第一个数组是匹配到的标签第二个数组是文档对象也有可能是jquery对象或者dom元素不妨调用一下看看这些参数的值更清晰一点:
在html里面创建jquery对象
<script> $('<div><124/div>') </script>
然后再jQuery源码中查看参数
} else { console.log(match[1]); console.log(doc); ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; }
在浏览器中运行的结果
<div><124/div> #document<!DOCTYPE html><html>…</html>
再换一个特殊一点的
$('<div><124/div>',{'class':'test'});
在浏览器中运行的结果
<div><124/div> jquery-1.7.1.js:157 Object {class: "test"}
这个时候的doc就是普通对象啦,至于第三个参数是在domManip方法中传递的是操作DOM的方法先不管。
紧接着声明了5个变量,fragment指向稍后可能创建的文档片段DocumentFragment;cacheable表示是否满足缓存条件,只有满足的才能进行缓存操作;cacheresults是从缓存对象读取的文档片段包含了缓存的DOM元素;变量doc表示创建文档片段的文档对象;first取得是数组的第一个元素因为也有可能创建多个元素比如$('<div>123</div>,<a>123</a>')。接着看源码:
// nodes may contain either an explicit document object, // a jQuery collection or context object. // If nodes[0] contains a valid object to assign to doc if ( nodes && nodes[0] ) { doc = nodes[0].ownerDocument || nodes[0]; }
这段代码是处理文档对象的,上面也分析过了传递过来的nodes里面可能是文档对象document也有可能是普通对象也有可能是jQuery对象或者DOM元素
如果nodes存在而且不是空的话先尝试获得它的ownerDocument把他赋值给doc,说白了就是先修正DOM元素的情况。
// Ensure that an attr object doesn't incorrectly stand in as a document object // Chrome and Firefox seem to allow this to occur and will throw exception // Fixes #8950 if ( !doc.createDocumentFragment ) { doc = document; }
doc就是修正文档对象的,显然上一个方法不太给力只处理了DOM元素的情况,紧接着这一段就是处理不是DOM元素的情况,如果不存在createDocumentFragment的方法说明不是DOM元素就直接让doc等于document,这里doc修正完毕会始终是创建文档片段的文档对象。
// Only cache "small" (1/2 KB) HTML strings that are associated with the main document // Cloning options loses the selected state, so don't cache them // IE 6 doesn't like it when you put <object> or <embed> elements in a fragment // Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache // Lastly, IE6,7,8 will not correctly reuse cached fragments that were created from unknown elems #10501 if ( args.length === 1 && typeof first === "string" && first.length < 512 && doc === document && first.charAt(0) === "<" && !rnocache.test( first ) && (jQuery.support.checkClone || !rchecked.test( first )) && (jQuery.support.html5Clone || !rnoshimcache.test( first )) ) { cacheable = true; cacheresults = jQuery.fragments[ first ]; if ( cacheresults && cacheresults !== 1 ) { fragment = cacheresults; } }
这一段是处理缓存的的,首先要判断一下是不是符合缓存条件其中包括了
1、数组 args 的长度为 1,且第一个元素是字符串,即数组 args 中只含有一段 HTML代码。
2、 HTML 代码的长度小于 512(1/2KB) ,否则可能会导致缓存占用的内存过大。
3、 文档对象 doc 是当前文档对象,即只缓存为当前文档创建的 DOM 元素,不缓存其他框架(iframe)的。
4、 HTML 代码以左尖括号开头,即只缓存 DOM 元素,不缓存文本节点。
5、 HTML 代码中不能含有以下标签:<script>、<object>、<embed>、<option>、<style>。
6、 当前浏览器可以正确地复制单选按钮和复选框的选中状态 checked,或者 HTML 代码中的单选按钮和复选按钮没有被选中。
7、 当前浏览器可以正确地复制 HTML5 元素,或者 HTML 代码中不含有 HTML5 标签
后面四个条件用到了jquery的功能检测和正则主要是判断这些条件,一旦这些条件满足就把cacheable属性改成true表示符合缓存条件。
然后尝试着从jQuery.fragments对象中读取缓存结果,如果之前已经创建过dom元素了在jQuery.fragments对象中有结果那就直接调缓存结果jQuery.fragments 方法在jQuery.buildFragment方法之后声明用于保存缓存的文档片段默认为空。
如果cacheresults有值而且不为1就调缓存结果,把结果赋值给fragment,为什么要判断为1呢?看到后面就明白了。
jQuery.fragments = {};
接着看源码:
if ( !fragment ) { fragment = doc.createDocumentFragment(); jQuery.clean( args, doc, fragment, scripts ); }
fragment没有值说明没有缓存结果那就自己创建文档片段交给fragment然后调用clean方法转成dom元素。其实最终是clean方法处理的,找个方法比较复杂下一篇再分析吧。
if ( cacheable ) { jQuery.fragments[ first ] = cacheresults ? fragment : 1; } return { fragment: fragment, cacheable: cacheable };
接着往后看,如果符合缓存条件就把结果缓存起来保存到jQuery.fragments这个对象中去,它的结果取决于cacheresults是否存在,不存在就是1这就是为什么前面要判断cacheresults !== 1的原因。最终返回一个对象包含文档片段和是否满足缓存条件的结果其他地方再根据返回的结果进行相应的处理。
现在再来完整的看一下这个方法的用法:首先当假设我们第一次传入一个复杂标签创建jQuery对象时会首先修正文档对象保证为document,然后会判断是否满足缓存条件,然后找到存储缓存文档片段的对象jQuery.fragments看是否有值把结果就赋值给cacheresults,cacheresults有值在把他赋值给fragment,由于是第一次使用肯定是没有值得f ( !fragment )是为真的这个时候调用js方法fragment = doc.createDocumentFragment();创建文档片段fragment,最后用clean方法处理文档片段构造jQuery对象。假设我们传递的参数是支持缓存的那么cacheable为真但是cacheresults又不存在所以jQuery.fragments[ first ]值变成了一。当我们第二次在使用相同的参数时,由于不满足cacheresults !== 1的条件所以依然会自己创建但是此时的cacheresults是1并非没有值这个时候才把创建好的文档片段fragment放到jQuery.fragments中,当我们第三次使用同样的参数时就会自动调用jQuery.fragments的结果啦,所以是第三次开始才能使用缓存的。为什么jQuery要在第三次才能调用缓存呢?完全可以在第一次就把缓存结果保存在jQuery.fragments去第二次就可以调用了,这里可能有其他的用处我暂时还没有想好。最后附上完整源码:
1 Query.buildFragment = function( args, nodes, scripts ) { 2 var fragment, cacheable, cacheresults, doc, 3 first = args[ 0 ]; 4 5 // nodes may contain either an explicit document object, 6 // a jQuery collection or context object. 7 // If nodes[0] contains a valid object to assign to doc 8 if ( nodes && nodes[0] ) { 9 doc = nodes[0].ownerDocument || nodes[0]; 10 } 11 12 // Ensure that an attr object doesn't incorrectly stand in as a document object 13 // Chrome and Firefox seem to allow this to occur and will throw exception 14 // Fixes #8950 15 if ( !doc.createDocumentFragment ) { 16 doc = document; 17 } 18 19 // Only cache "small" (1/2 KB) HTML strings that are associated with the main document 20 // Cloning options loses the selected state, so don't cache them 21 // IE 6 doesn't like it when you put <object> or <embed> elements in a fragment 22 // Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache 23 // Lastly, IE6,7,8 will not correctly reuse cached fragments that were created from unknown elems #10501 24 if ( args.length === 1 && typeof first === "string" && first.length < 512 && doc === document && 25 first.charAt(0) === "<" && !rnocache.test( first ) && 26 (jQuery.support.checkClone || !rchecked.test( first )) && 27 (jQuery.support.html5Clone || !rnoshimcache.test( first )) ) { 28 29 cacheable = true; 30 31 cacheresults = jQuery.fragments[ first ]; 32 if ( cacheresults && cacheresults !== 1 ) { 33 fragment = cacheresults; 34 } 35 } 36 37 if ( !fragment ) { 38 fragment = doc.createDocumentFragment(); 39 jQuery.clean( args, doc, fragment, scripts ); 40 } 41 42 if ( cacheable ) { 43 jQuery.fragments[ first ] = cacheresults ? fragment : 1; 44 } 45 46 return { fragment: fragment, cacheable: cacheable }; 47 }; 48 49 jQuery.fragments = {};