Welcome 微信登录

首页 / 脚本样式 / JavaScript / Jquery-1.9.1源码分析系列(十一)之DOM操作

DOM操作包括append、prepend、before、after、replaceWith、appendTo、prependTo、insertBefore、insertAfter、replaceAll。其核心处理函数是domManip。
DOM操作函数中后五种方法使用的依然是前面五种方法,源码
jQuery.each({appendTo: "append",prependTo: "prepend",insertBefore: "before",insertAfter: "after",replaceAll: "replaceWith"}, function( name, original ) {jQuery.fn[ name ] = function( selector ) {var elems,i = 0,ret = [],insert = jQuery( selector ),last = insert.length - 1;for ( ; i <= last; i++ ) {elems = i === last ? this : this.clone(true);jQuery( insert[i] )[ original ]( elems );//现代浏览器调用apply会把jQuery对象当如数组,但是老版本ie需要使用.get()core_push.apply( ret, elems.get() );}return this.pushStack( ret );};});
浏览器原生的插入节点的方法有两个:appendChild和inserBefore,jQuery利用这两个方法拓展了如下方法
jQuery.fn.append使用this.appendChild( elem )
jQuery.fn.prepend使用this.insertBefore( elem, this.firstChild )
jQuery.fn.before使用this.parentNode.insertBefore( elem, this );
jQuery.fn.after使用this.parentNode.insertBefore( elem, this.nextSibling );
jQuery.fn.replaceWith 使用this.parentNode.insertBefore( elem, this.nextSibling);
看一个例子的源码(jQuery.fn.append)
append: function() {return this.domManip(arguments, true, function( elem ) {if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {this.appendChild( elem );}});}
根据上面的源码。猜测domManip的作用是遍历当前jQuery对象所匹配的元素,然后每个元素调用传入的回调,并将要插入的节点(如果是字符串那么需要创建文档碎片节点)作为传入的回调的参数;并执行传入的回调。
接下来分析domManip,看猜测是否正确。dom即Dom元素,Manip是Manipulate的缩写,连在一起的字面意思就是就是Dom操作。
a. domManip: function( args, table, callback )解析

args 待插入的DOM元素或HTML代码
table 是否需要修正tbody,这个变量是优化的结果
callback 回调函数,执行格式为callback.call( 目标元素即上下文, 待插入文档碎片/单个DOM元素 )
先看流程,再看细节
第一步,变量初始化。其中iNoClone在后面会用到,如果当前的jQuery对象所匹配的元素不止一个(n > 1)的话,意味着构建出来的文档碎片需要被n用到,则需要被克隆(n-1)次,加上碎片文档本身才够n次使用;value 是第一个参数args的第一个元素,后面会对value是函数做特殊处理;
var first, node, hasScripts,scripts, doc, fragment,i = 0,l = this.length,set = this,iNoClone = l - 1,value = args[0],isFunction = jQuery.isFunction( value );
第二步,处理特殊下要将当前jQuery对象所匹配的元素一一调用domManip。这种特殊情况有两种:第一种,如果传入的节点是函数(即value是函数)则需要当前jQuery对象所匹配的每个元素都将函数计算出的值作为节点代入domManip中处理。第二种,webkit下,我们不能克隆文含有checked的文档碎片;克隆的文档不能重复使用,那么只能是当前jQuery对象所匹配的每个元素都调用一次domManip处理。
//webkit下,我们不能克隆文含有checked的档碎片if ( isFunction || !( l <= 1 || typeof value !== "string" || jQuery.support.checkClone || !rchecked.test( value ) ) ) {return this.each(function( index ) {var self = set.eq( index );//如果args[0]是函数,则执行函数返回结果替换原来的args[0]if ( isFunction ) {args[0] = value.call( this, index, table ? self.html() : undefined );}self.domManip( args, table, callback );});}
第三步,处理正常情况,使用传入的节点构建文档碎片,并插入文档中。这里面构建的文档碎片就需要重复使用,区别于第二步的处理。这里面需要注意的是如果是script节点需要在加载完成后执行。顺着源码顺序看一下过程
构建文档碎片
fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this );first = fragment.firstChild;if ( fragment.childNodes.length === 1 ) {fragment = first;}
分离出其中的script,这其中有一个函数disableScript更改了script标签的type值以确保安全,原来的type值是"text/javascript",改成了"true/text/javascript"或"false/text/javascript"
scripts = jQuery.map( getAll( fragment, "script" ), disableScript );hasScripts = scripts.length;
文档碎片插入页面
for ( ; i < l; i++ ) {node = fragment;if ( i !== iNoClone ) {node = jQuery.clone( node, true, true );// Keep references to cloned scripts for later restorationif ( hasScripts ) {jQuery.merge( scripts, getAll( node, "script" ) );}}callback.call(table && jQuery.nodeName( this[i], "table" ) ?findOrAppend( this[i], "tbody" ) :this[i],node,i);}
执行script,分两种情况,远程的使用ajax来处理,本地的直接执行。
if ( hasScripts ) {doc = scripts[ scripts.length - 1 ].ownerDocument;// Reenable scriptsjQuery.map( scripts, restoreScript );//在第一个文档插入使执行可执行脚本for ( i = 0; i < hasScripts; i++ ) {node = scripts[ i ];if ( rscriptType.test( node.type || "" ) &&!jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) {if ( node.src ) {// Hope ajax is available...jQuery.ajax({url: node.src,type: "GET",dataType: "script",async: false,global: false,"throws": true});} else {jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) );}}}}
b. dom操作拓展
jQuery.fn.text
jQuery.fn.text: function( value ) {return jQuery.access( this, function( value ) {return value === undefined ?jQuery.text( this ) :this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) );}, null, value, arguments.length );}
  最终执行value === undefined ? jQuery.text( this ) : this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) );
其中jQuery.text = Sizzle.getText;
jQuery.fn.html
函数使用jQuery.access来处理
jQuery.fn.html: function( value ) {return jQuery.access( this, function( value ) {...}, null, value, arguments.length );}
如果没有参数表示是取值
if ( value === undefined ) {return elem.nodeType === 1 ?elem.innerHTML.replace( rinlinejQuery, "" ) :undefined;}
否则看是否能用innerHTML添加内容。点击参考兼容问题
//看看我们是否可以走了一条捷径,只需使用的innerHTML//需要执行的代码script|style|link等不能使用innerHTML//htmlSerialize:确保link节点能使用innerHTML正确序列化,这就需要在IE浏览器的包装元素//leadingWhitespace:IE strips使用.innerHTML需要以空白开头//不是需要额外添加结束标签或外围包装标签的元素if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) &&( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) &&!wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) {value = value.replace( rxhtmlTag, "<$1></$2>" );try {for (; i < l; i++ ) {//移除元素节点和缓存,阻止内存泄漏elem = this[i] || {};if ( elem.nodeType === 1 ) {jQuery.cleanData( getAll( elem, false ) );elem.innerHTML = value;}}elem = 0;//如果使用innerHTML抛出异常,使用备用方法} catch(e) {}}
如果不能使用innerHTML或使用不成功(抛出异常),则使用备用方法append
//备用方法,使用append添加节点if ( elem ) {this.empty().append( value );}  jQuery.fn.wrapAll(用单个标签将所有匹配元素包裹起来)   处理步骤:  传入参数是函数则将函数结果传入if ( jQuery.isFunction( html ) ) {  return this.each(function(i) {    jQuery(this).wrapAll( html.call(this, i) );  });}  创建包裹层//获得包裹标签 The elements to wrap the target aroundvar wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);if ( this[0].parentNode ) {  wrap.insertBefore( this[0] );}
用包裹裹住当前jQuery对象
wrap.map(function() {  var elem = this;  while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {    elem = elem.firstChild;  }   return elem;}).append( this );
注意:当前jQuery对象匹配的元素最好只有一个,如果有多个的话不推荐使用,这种情况慎用,后面举例可以看到。
简单的例子,原DOM为(后面都使用这个例子)
<div id="center" class="center"><div id="ss" class="center"><input type="submit" id="left" class="left"></div></div><div class="right">我是right</div>  $("#center").wrapAll("<p></p>")后,dom变成了<p>  <div id="center" class="center">    <div id="ss" class="center">      <input type="submit" id="left" class="left">    </div>  </div></p><div class="right">我是right</div>
慎用:如果当前jQuery所匹配的元素不止一个,例如原DOM执行$("div").wrapAll(“<p></p>”)后结果DOM变成
<p>  <div id="center" class="center"></div>  <div id="ss" class="center"><input type="submit" id="left" class="left"></div>  <div class="right">我是right</div></p>
看到结果了吧,本来#center是#ss的父节点,结果变成了#ss的兄弟节点。
jQuery.fn.wrapInner(在每个匹配元素的所有子节点外部包裹指定的HTML结构)
处理步骤:
传入参数是函数则将函数结果传入
if ( jQuery.isFunction( html ) ) {return this.each(function(i) {jQuery(this).wrapInner( html.call(this, i) );});}
遍历jQuery对象数组,获取每个元素包含的内容(所有子节点)contents,然后使用warpAll包裹住contents
return this.each(function() {var self = jQuery( this ),contents = self.contents();if ( contents.length ) {contents.wrapAll( html );} else {self.append( html );}});
还是使用上面的例子中的原DOM,执行$("div").wrapInner("<p></p>")后结果DOM变成
<div id="center" class="center">  <p>    <div id="ss" class="center">      <p>        <input type="submit" id="left" class="left">      </p>    </div>  </p></div><div class="right">  <p>    我是right  </p></div>
jQuery.fn.wrap(在每个匹配元素外部包裹指定的HTML结构)
对jQuery的每个元素分别使用wrapAll包裹一下
return this.each(function(i) {jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html );});
行$("div").wrap("<p></p>")后结果DOM变成
<p>  <div id="center" class="center">    <p>      <div id="ss" class="center">        <input type="submit" id="left" class="left">      </div>    </p>  </div></p><p>  <div class="right">我是right</div></p>
 jQuery.fn.unwrap(移除每个匹配元素的父元素)
使用replaceWith用匹配元素父节点的所有子节点替换匹配元素的父节点。当然了父节点是body/html/document肯定是移除不了的
return this.parent().each(function() {if ( !jQuery.nodeName( this, "body" ) ) {jQuery( this ).replaceWith( this.childNodes );}}).end();  执行$("div").wrap()后结果DOM变成<div id="ss" class="center">  <input type="submit" id="left" class="left"></div><div class="right">我是right</div>
jQuery.fn.remove(从文档中移除匹配的元素)
你还可以使用选择器进一步缩小移除的范围,只移除当前匹配元素中符合指定选择器的部分元素。
与detach()相比,remove()函数会同时移除与元素关联绑定的附加数据( data()函数 )和事件处理器等(detach()会保留)。
for ( ; (elem = this[i]) != null; i++ ) {if ( !selector || jQuery.filter( selector, [ elem ] ).length > 0 ) {// detach传入的参数keepData为true,不删除缓存if ( !keepData && elem.nodeType === 1 ) {//清除缓存jQuery.cleanData( getAll( elem ) );}if ( elem.parentNode ) {if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) {setGlobalEval( getAll( elem, "script" ) );}elem.parentNode.removeChild( elem );}}}
可以看到其中有一个重要的函数cleanData,该方法是用来清除缓存:遍历每一个节点元素,对每一个节点元素做一下处理:
1.获取当前元素对应的缓存
id = elem[ internalKey ];data = id && cache[ id ];
2.如果有绑定事件,则遍历解绑事件
if ( data.events ) {for ( type in data.events ) {if ( special[ type ] ) {jQuery.event.remove( elem, type );//这是一个快捷方式,以避免jQuery.event.remove的开销} else {jQuery.removeEvent( elem, type, data.handle );}}}
3.如果jQuery.event.remove没有移除cache,则手动移除cache。其中IE需要做一些兼容处理,而且最终会将删除历史保存如core_deletedIds中
//当jQuery.event.remove没有移除cache的时候,移除cacheif ( cache[ id ] ) {delete cache[ id ];//IE不允许从节点使用delete删除expando特征,//也能对文件节点使用removeAttribute函数;//我们必须处理所有这些情况下,if ( deleteExpando ) {delete elem[ internalKey ];} else if ( typeof elem.removeAttribute !== core_strundefined ) {elem.removeAttribute( internalKey );} else {elem[ internalKey ] = null;}core_deletedIds.push( id );}
jQuery.fn.detach
detach: function( selector ) {return this.remove( selector, true );},

jQuery.fn.empty(清空每个匹配元素内的所有内容(所有子节点))
函数将会移除每个匹配元素的所有子节点(包括文本节点、注释节点等所有类型的节点),会清空相应的缓存数据。
for ( ; (elem = this[i]) != null; i++ ) {//防止内存泄漏移除元素节点缓存if ( elem.nodeType === 1 ) {jQuery.cleanData( getAll( elem, false ) );}//移除所有子节点while ( elem.firstChild ) {elem.removeChild( elem.firstChild );}// IE<9,select节点需要将option置空if ( elem.options && jQuery.nodeName( elem, "select" ) ) {elem.options.length = 0;}}