jQuery源码分析之buildFragment方法和clone方法

buildFragment方法源码:

var rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi;
var strundefined=undefined;
var rtagName = /<([\w:]+)/;
var rtbody = /<tbody/i;
var rhtml = /<|&#?\w+;/;
var rscriptType = /^$|\/(?:java|ecma)script/i;
var support=$.support;
function getAll( context, tag ) {
	var elems, elem,
		i = 0,
		found = typeof context.getElementsByTagName !== strundefined ? context.getElementsByTagName( tag || "*" ) :
			typeof context.querySelectorAll !== strundefined ? context.querySelectorAll( tag || "*" ) :
			undefined;
//如果getElementsByTagName和 querySelectorAll 都不存在那么返回undefined,通过jQuery.nodeName来选择结果
	if ( !found ) {
		for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) {
			if ( !tag || jQuery.nodeName( elem, tag ) ) {
				found.push( elem );
			} else {
				jQuery.merge( found, getAll( elem, tag ) );
			}
		}
	}
	return tag === undefined || tag && jQuery.nodeName( context, tag ) ?
		jQuery.merge( [ context ], found ) :
		found;
}
var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" +
		"header|hgroup|mark|meter|nav|output|progress|section|summary|time|video";
function createSafeFragment( document ) {
	var list = nodeNames.split( "|" ),
		safeFrag = document.createDocumentFragment();
	if ( safeFrag.createElement ) {
		while ( list.length ) {
			safeFrag.createElement(
				list.pop()
			);
		}
	}
	return safeFrag;
}
wrapMap = {
		option: [ 1, "<select multiple='multiple'>", "</select>" ],
		legend: [ 1, "<fieldset>", "</fieldset>" ],
		area: [ 1, "<map>", "</map>" ],
		param: [ 1, "<object>", "</object>" ],
		thead: [ 1, "<table>", "</table>" ],
		tr: [ 2, "<table><tbody>", "</tbody></table>" ],
		col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
		td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
		// IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags,
		// unless wrapped in a div with non-breaking characters in front of it.
		_default: support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X<div>", "</div>"  ]
	}
	//domManip调用方式:fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this );
	 function buildFragment1( elems, context, scripts, selection ) {
		var j, elem, contains,
			tmp, tag, tbody, wrap,
			l = elems.length,
			// Ensure a safe fragment
			safe = createSafeFragment( context ),		 
			nodes = [],
			i = 0;	
        //循环elems对象
		for ( ; i < l; i++ ) {
			elem = elems[ i ];
            //如果elem存在或者elem是0
			if ( elem || elem === 0 ) {
				// Add nodes directly
				//如果是object那么直接放入nodes对象里面,以后直接添加到documentFragment中!
				if ( jQuery.type( elem ) === "object" ) {
					jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );
				// Convert non-html into a text node
				//rhtml = /<|&#?\w+;/
				//把非html的内容转化为文本节点,也就是不是html标签,html标签要么开头是<要么开头是"&",构建文本节点!
				} else if ( !rhtml.test( elem ) ) {
					nodes.push( context.createTextNode( elem ) );     
				// Convert html into DOM nodes
				} else {
					//把html变成DOM节点,添加到safe的documentFragment上,这里是空元素!
					tmp = tmp || safe.appendChild( context.createElement("div") );      
					// Deserialize a standard representation
					//rtagName = /<([\w:]+)/获取标签名称
					tag = (rtagName.exec( elem ) || [ "", "" ])[ 1 ].toLowerCase();
					//打印[div,span]
					//alert(tag);
					//如果标签是options等标签
					wrap = wrapMap[ tag ] || wrapMap._default;
					//[0,,][0,,]
					//alert(wrap)
                                       //为元素添加内容
					//rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi
					//<div/>'.replace( rxhtmlTag, '<$1></$2>' );输出<html></html>
					tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1></$2>" ) + wrap[2];
					//打印[<div></div>] [<span></span>]
				   //  alert(tmp.innerHTML)
					// Descend through wrappers to the right content
					j = wrap[0];
					//打印0,因为不再wrapMap中间
					//alert(j);
					//获取数组第一个元素,移动到添加内容的部分
					while ( j-- ) {
						tmp = tmp.lastChild;
					}       
					// Manually add leading whitespace removed by IE
					//手动添加被IE移除的开头的空白节点
					if ( !support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
						nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) );
					}
					// Remove IE's autoinserted <tbody> from table fragments
					//异常IE自动为table添加的tbody标签
					if ( !support.tbody ) {
						// String was a <table>, *may* have spurious <tbody>
						//rtbody = /<tbody/i;如果没有tbody那么获取firstChild为tr
						elem = tag === "table" && !rtbody.test( elem ) ?
							tmp.firstChild :
							// String was a bare <thead> or <tfoot>
							//自己传入的是thread标签,<table>是自动被包裹上去的!但是elem没有tbody标签
							wrap[1] === "<table>" && !rtbody.test( elem ) ?
								tmp :
								0;
						j = elem && elem.childNodes.length;
						while ( j-- ) {
//移除tbody标签,如果是自己传递的参数是table标签,因为wrapMap没有table标签,那么就从table开始遍历寻找tbody,如果是自己传递的thead标签
  //通过上面的while循环temp也已经是jQuery自己添加的table标签了,于是从从temp也就是自己的添加的table开始就可以了!
				if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) {
								elem.removeChild( tbody );
							}
						}
					}
			//打印[object HTMLDivElement][object HTMLSpanElement]把我传入的div,和span封装到上面的空对象tmp里面
					//alert(tmp.childNodes[0]);
                 //把temp下的childNodes全部合并
					jQuery.merge( nodes, tmp.childNodes );
					//已经添加过了,那上面的tmp清空
					// Fix #12392 for WebKit and IE > 9
					tmp.textContent = "";
            		//在IE下进行清空
					// Fix #12392 for oldIE
					while ( tmp.firstChild ) {
						tmp.removeChild( tmp.firstChild );
					}		        
					// Remember the top-level container for proper cleanup
					//记住最外层的容器的属性,形成<div><div></div></div>
					tmp = safe.lastChild;
					//[object HTMLDivElement],[object HTMLDivElement]
					//alert(tmp.outerHTML);
				
				}//End of else
			}//End of if
		}//End of for
		// Fix #11356: Clear elements from fragment
		 //最后一次保存了tmp,要清除,也就是这个documentFragment中作为工具的那个div
		 //也就是最外层的div可以被删除了
		if ( tmp ) {
			safe.removeChild( tmp );
		}
		// Reset defaultChecked for any radios and checkboxes
		// about to be appended to the DOM in IE 6/7 (#8060)
		if ( !support.appendChecked ) {
			jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked );
		}
		i = 0;
		while ( (elem = nodes[ i++ ]) ) {
			// #4087 - If origin and destination elements are the same, and this is
			// that element, do not do anything
			//如果传入了第四个参数,同时elem也在第四个参数里面,那么什么也不做!如domManip函数里面传入this
			//也就是调用对象,所以说如果传入的对象已经在调用对象里面,那么什么也不做,见下面的例子:
			if ( selection && jQuery.inArray( elem, selection ) !== -1 ) {
				continue;
			}
            //创建的Element对象的document是否包括该元素!
			contains = jQuery.contains( elem.ownerDocument, elem );
			//打印false
             // alert(contains);
			// Append to fragment
			//把创建的DOM对象放在fragment上面,同时找到这个DOM元素上面的所有的script标签!
			tmp = getAll( safe.appendChild( elem ), "script" );
             //如果这个创建的DOM对象在当前的document上面,那么在全局作用域里面执行JS代码,如果是通过标签建立的DOM那么这里是false
			// Preserve script evaluation history
			if ( contains ) {
				setGlobalEval( tmp );
			}
            //如果调用buildFragment时候传入了script是集合那么把该DOM元素上的script添加进去!
			//在parseHTML方法中调用是: jQuery.buildFragment( [ data ], context, scripts );这里面的script可以是数组
			// Capture executables
			if ( scripts ) {
				j = 0;
				while ( (elem = tmp[ j++ ]) ) {
					//获取该创建的DOM对象的所有的script标签进行遍历!如果type是ecmascript或者javascript
					if ( rscriptType.test( elem.type || "" ) ) {
					//把这个创建的元素对象的script放入传入的第三个参数scripts集合中
						scripts.push( elem );
					}
				}
			}
		}
		tmp = null;
       //把添加的safe返回,是documentFragment对象
		return safe;
	}
buildFragment1(["<div id='n1'><script type='text/javascript'>alert('invoked!');<\/script></div>","<span id='n2'/>"],$("#content")[0].ownerDocument,false);
fixDefaultCheck函数源码,用于修正fragment中defaultChecked属性:

function fixDefaultChecked( elem ) {
	if ( rcheckableType.test( elem.type ) ) {
		elem.defaultChecked = elem.checked;
	}
}

 思路:首先创建一个空的documentFragment然后在上面添加一个空的div元素,然后把要创建的对象放在该div里面作为子元素,同时把创建的DOM对象放在nodes集合里面,所以经过for循环过后所有的创建的DOM元素全部已经在nodes集合里面了!但是有一点要注意,那上面的例子说明一下: 

第一次创建的temp元素是<div><div id="n1"></div></div>而且这个外层div在documentFrament上面,内部加入到nodes集合以后,为了防止再一次创建的div放在documentFrament,我们用temp保存了外层div的引用,之所以是外层div的引用是因为我们通过textContent把内部的内容全部清空了,这整个过程都是在documentFragment对象上操作的!所以第二次创建的<span id="n2"></span>依然可以在这个documentFrament对象上操作,最后得到NodeList集合,集合中存放了上面创建的div和span元素! 

细节部分:

(1)IE会自动移除前面的空格,也就是用innerHTML添加内容的时候IE会自动移除前面的空格,所以前面必须判断节点是否有空格并且手动加上才行!

(2)对于replace方法来说如果字符串不满足正则表达式那么不会对字符串有任何的影响,所以如果传入参数为<div>xxxx</div>那么就会原封不动的构建出DOM,上面的正则表达式只会对<div/>这种标签起作用!

(3)上面的wrapMap的第一个数字参数就是表示应该往下移动几层,因为如果外面包裹了元素table,tbody,tr那么只有在第三层以后从才能添加元素!

(4)为了移除元素内部所有的内容可以用elem.textContent="",但是为了兼容IE<9浏览器要用while(elem.firstChild){elem.removeChild(elem.firstChild)}

(5)support.tbody表示是否存在tbody标签,存在为false,不存在为true.上面就是说存在tbody标签的情况下需要有针对性的移除空的tbody标签!

(6)appendChecked检测fragment中的checkbox能够被成功赋值,如果可以返回true,如果不可以返回false,上面的if语句执行表示没有被成功赋值,于是拿着上面克隆好的DOM节点的checked属性,把checked属性设置为defaultChecked属性!

从append方法和domManip方法看buildFragment源码:

append: function() {
		return this.domManip( arguments, function( elem ) {
			if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
				var target = manipulationTarget( this, elem );
				target.appendChild( elem );
			}
		});
	}

domManip里面调用buildFragment方法:

	fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this );
从该方法的调用和buildFragment的源码来说,如果按照这种方式调用append,如:$n1.append( document.getElementsByTagName("label"), $("i") );

那么append传入的参数,也就是buildFragments里面的args如果已经存在于this,也就是$n1当中,那么不会被添加!给出下面一个例子:

<div>
	<div id="child1">
		我是child1元素
	</div>
	<div id="child2">
		我是child2元素
	</div>
</div>
$("div").append($("#child1")[0],$("#child2")[0]);//发现不会有任何变化!也就是append调用domManip,然后调用buildFragment没有任何反应!
里面调用了setGlobalEval把所有的script标签标记为已经执行:(JS对象在 data中也是可以保存数据的!)

function setGlobalEval( elems, refElements ) {
	var elem,
		i = 0;
	for ( ; (elem = elems[i]) != null; i++ ) {
		jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) );
	}
}
buildFragment测试总结:

(1)如果传给buildFragment的第一个参数已经在第四个参数里面,那么该方法什么也不做,如append方法中传来的就是this,也就是调用对象,所以如果append参数在this里面那么调用buildFragment什么也不做!

(2)整个把所有的第一个参数转化为DOM Node的过程都是在documentFragment上面完成的,因为最外层的div会被保存起来参加每一次循环,而且该div一直在documentFragment上面!

下面我们分析clone方法:

cloneCopyEvent方法:(把数据和事件从一个对象复制到另外一个对象)

function cloneCopyEvent( src, dest ) {
	//只有Element才克隆
	if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) {
		return;
	}
	var type, i, l,
		//获取src元素下的所有的jQuery内部数据
		oldData = jQuery._data( src ),
		//内部调用internalData方法,并且调用jQuery.extend所以会使得dest具有olddata的所有数据!
		curData = jQuery._data( dest, oldData ),
		//获取events对象
		events = oldData.events;
	    if ( events ) {
		//删除handle空间
		delete curData.handle;
		//开辟events空间,成为cache[id]={events:{click:[function(){},funciton(){}],blur:[funciton(){},function(){}]}}
		curData.events = {};
		for ( type in events ) {
			//同一个事件可以有多个函数句柄
			for ( i = 0, l = events[ type ].length; i < l; i++ ) {
				//把所有的事件都绑定到dest上面
				jQuery.event.add( dest, type, events[ type ][ i ] );
			}
		}
	}
	// make the cloned public data object a copy from the original
	//如果是cache[id]={events:{},data:{}},那么把所有的数据复制一份到data域下面
	//如果调用的是$._data方法,那么这个data域下面是没有数据的
	if ( curData.data ) {
		curData.data = jQuery.extend( {}, curData.data );
	}
}
fixCloneNodeIssue方法源码:(用于解决在元素克隆问题不同浏览器不一致)
function fixCloneNodeIssues( src, dest ) {
	var nodeName, e, data;
	// We do not need to do anything for non-Elements
	if ( dest.nodeType !== 1 ) {
		return;
	}
	nodeName = dest.nodeName.toLowerCase();
	// IE6-8 copies events bound via attachEvent when using cloneNode.
	//IE6-8时候noCloneEvent返回false,也就是复制节点时候不复制事件
	if ( !support.noCloneEvent && dest[ jQuery.expando ] ) {
		//获取目标节点的数据cache[id]={}
		data = jQuery._data( dest );
         //如果数据对象中有events,cache[id]={events:{}}内部数据不是在data域下!
		for ( e in data.events ) {
			//把dest上面的所有事件全部移除掉!
			jQuery.removeEvent( dest, e, data.handle );
		}
         //移除该元素上面expando属性
		// Event data gets referenced instead of copied if the expando gets copied too
		dest.removeAttribute( jQuery.expando );
	}
    //如果dest是script,同时dest和src的text不相同
	// IE blanks contents when cloning scripts, and tries to evaluate newly-set text
	if ( nodeName === "script" && dest.text !== src.text ) {
		//首先把type改为"true/text/script",然后把值设为src的值
		disableScript( dest ).text = src.text;
		restoreScript( dest );
	// IE6-10 improperly clones children of object elements using classid.
	// IE10 throws NoModificationAllowedError if parent is null, #12132.
	//如果dest的nodeName是object
	} else if ( nodeName === "object" ) {
		if ( dest.parentNode ) {
			dest.outerHTML = src.outerHTML;
		}
		// This path appears unavoidable for IE9. When cloning an object
		// element in IE9, the outerHTML strategy above is not sufficient.
		// If the src has innerHTML and the destination does not,
		// copy the src.innerHTML into the dest.innerHTML. #10324
		if ( support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) {
			dest.innerHTML = src.innerHTML;
		}
  //IE6-8不能保存克隆对象的checked状态
	} else if ( nodeName === "input" && rcheckableType.test( src.type ) ) {
		// IE6-8 fails to persist the checked state of a cloned checkbox
		// or radio button. Worse, IE6-7 fail to give the cloned element
		// a checked appearance if the defaultChecked value isn't also set
        //把dest的defaultChecked和checked状态都设置为src的状态
		dest.defaultChecked = dest.checked = src.checked;
         //IE6-7把克隆节点的value值设置为空字符串,而不是on
		// IE6-7 get confused and end up setting the value of a cloned
		// checkbox/radio button to an empty string instead of "on"
		if ( dest.value !== src.value ) {
			dest.value = src.value;
		}
	// IE6-8 fails to return the selected option to the default selected
	// state when cloning options
	//IE6-8不会把源option的选择状态克隆,也就是克隆对象没有原来的option的选择状态!
	} else if ( nodeName === "option" ) {
		dest.defaultSelected = dest.selected = src.defaultSelected;
     //IE6-8不会把源对象的默认值正确克隆(这里指的是input对象)
	// IE6-8 fails to set the defaultValue to the correct value when
	// cloning other types of input fields
	} else if ( nodeName === "input" || nodeName === "textarea" ) {
		dest.defaultValue = src.defaultValue;
	}
clone方法源码:

clone: function( elem, dataAndEvents, deepDataAndEvents ) {
		var destElements, node, clone, i, srcElements,
			//如果在当前页面
			inPage = jQuery.contains( elem.ownerDocument, elem );
          //用HTML5的方法进行克隆
		if ( support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) {
			clone = elem.cloneNode( true );

		// IE<=8 does not properly clone detached, unknown element nodes
		} else {
			fragmentDiv.innerHTML = elem.outerHTML;
			fragmentDiv.removeChild( clone = fragmentDiv.firstChild );
		}  
		if ( (!support.noCloneEvent || !support.noCloneChecked) &&
				(elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) {
			// We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2
			//得到克隆节点下面所有的子节点
			destElements = getAll( clone );
             //得到源节点下的所有的节点
			srcElements = getAll( elem );
			// Fix all IE cloning issues
			for ( i = 0; (node = srcElements[i]) != null; ++i ) {
				// Ensure that the destination node is not null; Fixes #9587
				if ( destElements[i] ) {
					//源节点到目标节点
					fixCloneNodeIssues( node, destElements[i] );
				}
			}
		}
         //把事件从源节点克隆到目标节点上
		// Copy the events from the original to the clone
		if ( dataAndEvents ) {
			//如果深度克隆数据和事件,因为同一个click事件可能有多个回调函数,所以要深度克隆!
			if ( deepDataAndEvents ) {
				srcElements = srcElements || getAll( elem );
				destElements = destElements || getAll( clone );
                  //把src上的事件克隆到目标节点上!
				for ( i = 0; (node = srcElements[i]) != null; i++ ) {
					cloneCopyEvent( node, destElements[i] );
				}
			} else {
				cloneCopyEvent( elem, clone );
			}
		}
		// Preserve script evaluation history
		destElements = getAll( clone, "script" );
		if ( destElements.length > 0 ) {
			//把所有的克隆DOM对象的script设置为已经执行过了
			setGlobalEval( destElements, !inPage && getAll( elem, "script" ) );
		}
		destElements = srcElements = node = null;

		// Return the cloned set
		return clone;
	}
clone方法总结:

注意:出于性能原因考虑,clone()函数不会复制某些表单元素的动态,例如用户在<textarea>输入的内容、用户在<select>中选择的选项(但是会保存option的defaultSelect和input的defaultValue)。不过<input>元素的动态将会被复制,例如用户在text中输入的内容、用户对checkbox的选中状态。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值