jQuery源码分析系列:.domManip() .buildFragment() .clean()
.domManip(args,table,callback):是jQuery DOM操作的核心函数,可以扩展出如下方法:
append/appendTo:
prepend/prependTo:
before/insertBefore:
after/insertAfter:
1.转换HTML为DOM元素,将args转换为DOM元素,并放在一个文档碎片中,调用jQueyr.buidFragment和jQuery.clean实现。
2.执行回调函数插入DOM元素。进行callback,将DOM元素作为参数传入,callbacks执行实际操作。
扩展内容:
insertAdjacentElement、insertAdjacentHTML、insertAdjacentText,在指定的位置插入DOM元素 HTML代码 文本。
insertAdjacentElement(sWhere,oElement/sText):在sWhere处插入oElement/sText内容。
可选值 |
功能 |
jQuery中的等价方法 |
beforeBegin |
object之前 |
.before() |
afterBegin |
前置,作为object的第一个子元素 |
.prepend() |
beforeEnd |
追加,作为object的最后一个子元素 |
.append() |
afterEnd |
object之后 |
.after() |
domManip(args, table, callback)源码:
执行步骤:
1.转换HTML代码为DOM元素
2.执行回调函数插入DOM元素
代码流程结构:
局部变量初始化->规避WebKit checked属性->支持参数为函数->转换HTML为DOM->执行回调函数插入DOM->执行脚本元素
参数说明:
args 待插入的DOM元素或HTML代码
table 是否需要修正tbody 这个变量是优化用的
callback 回调函数 执行格式为callback.call(目标元素即上下文,待插入文档碎片/单个DOM元素)
1 domManip: function( args, table, callback ) { 2 var results, first, fragment, parent, 3 value = args[0],//第一个元素,后边只针对args[0]进行检测 4 /* 5 在jQuery.buildFragment中会用到,脚本的执行在.domManip()的最后一行代码;jQuery.buildFragment中调用jQuery.clean时将 6 scripts作为参数传入;jQuery.clean如果遇到script标签,则将script放入scripts,条件是:标签名为script 并且 未指定type或type为text/javascript; 7 即支持插入script标签并执行;外联脚本通过jQuery.ajax以同步阻塞的方式请求然后执行,内联脚本通过jQuery.globalEval执行。 8 */ 9 scripts = []; 10 11 //规避WebKit checked属性 不能克隆包含了已选中多选按钮的文档碎片 12 //不能正确拷贝选中状态&&参数个数为3&&value(args[0])是字符串&&已选中的多选按钮 单选按钮 13 if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) { 14 return this.each(function() { 15 jQuery(this).domManip( args, table, callback, true ); 16 }); 17 } 18 19 //支持参数为函数 如果value为函数,则将value的执行结果作为args[0]的值 只能够处理一个 20 if ( jQuery.isFunction(value) ) {//value is function 21 return this.each(function(i) { 22 var self = jQuery(this); 23 //执行函数并返回结果 如果table为true 调用innerHTML修正tbody, 24 //用value的返回值替换args[0] 最后用修正过的args 迭代调用.domManip() 25 args[0] = value.call(this, i, table ? self.html() : undefined); 26 //再次调用domManip 27 self.domManip( args, table, callback ); 28 }); 29 } 30 31 //将HTML转换成DOM 32 if ( this[0] ) { 33 parent = value && value.parentNode; 34 35 //parent.nodeType === 11 检测父元素是文档碎片(DocumentFragment(nodeTYpe ===1)那么就不用重新创建文档碎片了 36 if ( jQuery.support.parentNode && parent && parent.nodeType === 11 && parent.childNodes.length === this.length ) { 37 results = { fragment: parent };//文档碎片就是现有的 38 39 } else { 40 //没有父元素或父元素不是文档碎片,则调用 jQuery.buildFragment 创建一个包含args的文档碎片,jQuery.buildFragment 41 //用到了缓存,重复的创建会被缓存下来(需满足一些条件讲到jQuery.buildFragment时会详细分析), 42 //jQuery.buildFragment返回的结构是: { fragment: fragment, cacheable: cacheable } 43 results = jQuery.buildFragment( args, this, scripts ); 44 } 45 46 fragment = results.fragment;//args 47 48 //获取第一个子元素first,first在后边用于判断是否需要修正tr的父元素为tbody。 49 //以第一个元素为准;如果只有一个子元素,那么可以省掉文档碎片;这么做可以更快的插入元素, 50 if ( fragment.childNodes.length === 1 ) {//childNodes:返回包含被选节点的子节点的 NodeList 51 first = fragment = fragment.firstChild; 52 } else { 53 first = fragment.firstChild; 54 } 55 56 //执行回调函数插入DOM元素 57 if ( first ) {//如果成功创建了DOM元素 58 //tr的父元素是tbody table指示是否需要修正tbody 59 table = table && jQuery.nodeName( first, "tr" );//节点有tr 60 //遍历当前jQuery对象中的匹配元素,缓存this.length 61 for ( var i = 0, l = this.length, lastIndex = l - 1; i < l; i++ ) { 62 callback.call(//执行callback 63 //如果是tr 修正目标元素即上下文 64 table ? 65 /* 66 function root( elem, cur ) {//cur干了什么? 不解 67 return jQuery.nodeName(elem, "table") ? //elem中是否有table标签 68 //返回第一个tbody或者创建一个tbody返回 69 (elem.getElementsByTagName("tbody")[0] || 70 elem.appendChild(elem.ownerDocument.createElement("tbody"))) : 71 elem; 72 } 73 */ 74 root(this[i], first) : //如果是tr 修正目标元素即上下文 75 this[i], 76 //克隆文档碎片 单个DOM元素 缓存的或者this中有多个元素; 77 results.cacheable || (l > 1 && i < lastIndex) ? 78 //克隆 79 jQuery.clone( fragment, true, true ) : 80 fragment 81 ); 82 } 83 } 84 //执行脚本元素 如果脚本数组scripts的长度大于0 则执行其中的脚本; jQuery.clean中 如果script标签则会放入脚本数组scripts中 85 //evalScript负责执行script元素,如果是外联脚本(即通过src引入 src = " ... " ),用jQuery.ajax同步请求src指定的地址并自动执行; 86 //如果是内联脚本(即写在script标签内),用jQuery.globalEval执行。 87 if ( scripts.length ) { 88 jQuery.each( scripts, evalScript ); 89 } 90 } 91 return this; 92 }
jQuery.buildFragment(args,nodes,scripts)源码分析:
执行步骤:
创建文档碎片
转换HTML代码为DOM元素
缓存
执行流程:
修正文档对象doc->是否符合缓存条件->创建文档碎片->转换HTML代码jQuery.clean()->文档碎片放入缓存->返回文档碎片和缓存状态
关于文档碎片:
DocumentFragment 接口表示文档的一部分(或一段)。更确切地说,它表示一个或多个邻接的 Document 节点和它们的所有子孙节点。
DocumentFragment 节点不属于文档树,继承的 parentNode 属性总是 null。
当请求把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。
这使得 DocumentFragment 成了有用的占位符,暂时存放那些一次插入文档的节点。
可以用 Document.createDocumentFragment() 方法创建新的空 DocumentFragment 节点。
也可以用 Range.extractContents() 方法 或 Range.cloneContents() 方法 获取包含现有文档的片段的 DocumentFragment 节点。
results = jQuery.buildFragment( args, this, scripts );
接受.domManip()传入的HTML代码,创建文档碎片DocumentFragment,然后调用jQuery.clean()在这个文档碎片上将HTML代码转换成DOM
1.如果插入多个DOM元素,可以先将这些DOM插入到文档碎片,然后将文档碎片插入文档。这时插入的是文档碎片的子孙节点,可以提高性能。
2.将重复的HTML代码转换为DOM元素,可以将转换后的DOM元素缓存起来,下次转换时,直接调用缓存。
参数说明:
args:待插入的DOM元素或HTML代码。
nodes:jQuery对象,从其中获取Docuemnt对象,doc = nodes[0].ownerDocuemnt || nodes[0]
scripts:脚本数组 依次传递 .domManip() -> jQuery.buildFragment() -> jQuery.clean()
1 jQuery.buildFragment = function(args,nodes,scripts){ 2 var fragment, 3 cacheable, 4 cacheresults, 5 doc, 6 first = args[0]; 7 8 if(nodes && nodes[0]){ 9 //ownerDocument 返回节点所属的根元素 10 //第一个节点的根节点或者这个节点 11 doc = nodes[0].ownerDocument || nodes[0]; 12 } 13 //如果没有createDocumentFragment属性,则doc为顶层文档 14 if(!doc.createDocumentFragment){ 15 doc = document; 16 } 17 18 //args长度等于1&&第一个元素时字符串&&字符串长度小于512&&主文档对象 && 第一个字符时左尖括号 && 支持缓存的标签(除了/<(?:script|object|embed|option|style)/i) 19 //&& 要么支持正确的checked属性赋值&&要么没有checked属性&&要么支持正确的checked属性赋值&&要么没有checked属性&&要么可以正确拷贝HTML5元素&&要么不是HTML5标签s 20 if(args.length === 1 && typeof first === "string" && first.length < 512 && doc === document && first.charAt(0) === "<" 21 && !rnocache.test(first) && (jQuery.support.checkClone || !rchecked.test(first)) && 22 (jQuery.support.html5Clone || !rnoshimcache.test(first))){ 23 //表示是否first满足缓存条件放入缓存 24 cacheable = true; 25 //从jQuery.fragments中查找缓存结果,jQuery.fragments是全局的文档碎片缓存对象 26 cacheresults = jQuery.fragments[first]; 27 //缓存名中,并不是1 而是真正的文档碎片 , 1表示第一次调用jQuery.buildFragment时设置 28 if(cacheresults && cacheresults !== 1){ 29 fragment = cacheresults; 30 } 31 } 32 /* 33 1.不符合缓存条件 34 2.符合缓存条件第一次调用jQuery.buildFragment()此时缓存中还没有 35 3.符合缓存条件 第二次调用jQuery.buildFragment,此时缓存中是1 36 */ 37 if(!fragment){ 38 fragment = doc.createDocumentFragment();//创建一个文档碎片 39 jQuery.clean(args,doc,fragment,scripts);//调用jQuery.clean将HTML字符串args转换成DOM 40 } 41 /* 42 如果符合缓存条件,将文档碎片放入缓存,放入的可能是文档碎片fragment或1,取决于缓存中的已有值; 43 如果已有值为undefined则为1,如果是1则为fragment 44 45 执行过程 46 第一次 设置为1,因为已有值为undefined 47 第二次 设置为fragment,已有值为1 48 第三次 开始取缓存中的值 49 */ 50 if(cacheable){ 51 jQuery.fragments[first] = cacheresults ? fragment : 1; 52 } 53 //返回文档碎片和缓存状态 54 return {fragment:fragment,cacheable:cacheable}; 55 }
clean()源码分析:
修正文档对象context
声明返回值
遍历带转换数组
遇到HTML代码开始转换
不是标签 创建TextNode
是标签 开始转换
修正XHTML风格的标签
创建临时div 插入到安全文档碎片
包裹HTML代码 设置innerHTML
如果包裹 剥去包裹元素
移除IE自动插入的空tbody
插入IE自动剔除的空白符
取到创建的DOM元素
修正IE6/7中defaultChecked属性
合并转换后的DOM元素
提取script元素
返回转换后的DOM元素数组
elems 待转换的HTML代码数组
context 文档对象 会调用context的createTExtNode方法和createElement方法
fragment 文档碎片 在其上插入div 在div上设置innerHTML
scripts 脚本数组
1 clean: function( elems, context, fragment, scripts ) { 2 //修正文档对象context 后面会调用createTextNode方法和createElement方法 3 var checkScriptType; 4 context = context || document; 5 if ( typeof context.createElement === "undefined" ) { 6 context = context.ownerDocument || context[0] && context[0].ownerDocument || document; 7 } 8 9 //声明返回值:ret转换后的DOM元素数组 返回值 10 var ret = [], 11 j;//j循环变量 后文循环删除空tbody和修正defaultChecked时用到 12 //遍历待转换的数组elems(如果是字符串,那就一个字符一个字符的创建TextNode) 13 for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { 14 //将数字转换成字符串 15 if ( typeof elem === "number" ) { 16 elem += ""; 17 } 18 //检测非法值, 19 if ( !elem ) { 20 continue; 21 } 22 23 //遇到HTML代码开始转换 24 if ( typeof elem === "string" ) { 25 //如果不是标签,就创建TextNode 文本节点 rhtml = /<|&#?\w+;/;<是HTML标签的起始符号,&#?\w+则是特殊符号的起始符号 26 if ( !rhtml.test( elem ) ) { 27 elem = context.createTextNode( elem ); 28 } else {//如果是标签,即使HTML代码 29 //修正XHTML风格的标签 :XHTML标签修正、保留HTML属性、过滤不需要修正的标签、正确修正未知标签、忽略大小写、多行匹配。 30 elem = elem.replace(rxhtmlTag, "<$1></$2>"); 31 32 //创建临时DIV 插入到安全文档碎片 33 //从HTML代码中提出来的标签 34 var tag = (rtagName.exec( elem ) || ["", ""])[1].toLowerCase(), 35 //wrap数组,其中放有tag的深度,父标签,父关闭标签 例如option对应 [ 1, "<select multiple='multiple'>", "</select>" ]; 36 wrap = wrapMap[ tag ] || wrapMap._default, 37 depth = wrap[0],//深度包裹了几层 例如option是1、tr是2、td是3,默认0即不包裹 38 //创建一个临时div容器 后边在改div上设置innerHTML 39 div = context.createElement("div"); 40 41 //加上包裹标签,设置innerHTML,由浏览器生成DOM元素 42 //例如:<option>1</option> 自动加上包裹标签,变成:<select multiple='multiple'><option>1</option></select> 43 div.innerHTML = wrap[1] + elem + wrap[2]; 44 45 //比如option自动包裹上select,depth为1,div剥一层是select;也就是说while循环最后的结果是包含了elem的父元素,即div成了elem的父元素; 46 //比如tr,div变成tbody;td,div变为tr;thead/tfoot,div变为table,以此类推;即修正elem的父元素。 47 while ( depth-- ) { 48 div = div.lastChild; 49 } 50 51 //移除IE自动插入的空tbody 52 if ( !jQuery.support.tbody ) { 53 // String was a <table>, *may* have spurious <tbody> 54 var hasBody = rtbody.test(elem), 55 tbody = tag === "table" && !hasBody ? 56 div.firstChild && div.firstChild.childNodes : 57 58 // String was a bare <thead> or <tfoot> 59 wrap[1] === "<table>" && !hasBody ? 60 div.childNodes : 61 []; 62 63 for ( j = tbody.length - 1; j >= 0 ; --j ) { 64 if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) { 65 tbody[ j ].parentNode.removeChild( tbody[ j ] ); 66 } 67 } 68 } 69 70 //插入IE自动剔除的空白符 71 if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { 72 div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild ); 73 } 74 //取到创建的DOM元素 取div的子元素集赋值给elem,elem会被合并到分绘制ret中; 75 //如果需要包裹div是待转换元素的父元素,如果不需要包裹 变量div就是DIV元素。所以这里可以简洁地取childNodes 76 elem = div.childNodes; 77 78 } 79 } 80 81 // 修正radios checkboxes的defaultChecked属性 IE6/7的bug 82 //在函数findInputs(elem)中找到 input 然后elem.defaultChecked = elem.checked 83 var len; 84 if ( !jQuery.support.appendChecked ) { 85 if ( elem[0] && typeof (len = elem.length) === "number" ) { 86 for ( j = 0; j < len; j++ ) { 87 findInputs( elem[j] ); 88 } 89 } else { 90 findInputs( elem ); 91 } 92 } 93 94 //合并转换后的DOM元素 95 if ( elem.nodeType ) {//如果是DOM元素直接push,elem本身就是DOM元素 96 ret.push( elem ); 97 } else {//elem含有多个元素,合并 98 ret = jQuery.merge( ret, elem ); 99 } 100 } 101 102 //提取script元素 103 if ( fragment ) { 104 //检测是否是scriptType元素 rscriptType = /\/(java|ecma)script/i;rscriptType 标签script的type属性是否是javascript或ecmascrip 105 checkScriptType = function( elem ) { 106 return !elem.type || rscriptType.test( elem.type ); 107 }; 108 for ( i = 0; ret[i]; i++ ) { 109 //如果ret[i]的标签名为script并且为指定type或type为text/javascript 放入scripts数组 110 if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) { 111 scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] ); 112 113 } else { 114 /* 115 取出ret[i]下可能有的的script元素,调用jQuery.grep( elems, callback, inv )过滤,返回其中 未指定type或type包含/javascript或/ecmascript 116 的script元素数组jsTags,将这个数组插入ret[i]之后,下次循环时检查。 117 118 i + 1,将找到的jsTags通过splice插入ret[i],for循环下次遍历ret时,从jsTags开始遍历;splice()方法用于插入、删除或替换数组的元素 119 */ 120 if ( ret[i].nodeType === 1 ) { 121 var jsTags = jQuery.grep( ret[i].getElementsByTagName( "script" ), checkScriptType ); 122 123 ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) ); 124 } 125 fragment.appendChild( ret[i] ); 126 } 127 } 128 } 129 //返回转换后的DOM数组 130 return ret; 131 },
posted on 2014-06-22 15:03 color_story 阅读(234) 评论(0) 编辑 收藏 举报