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){
varelems,
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());
}
returnthis.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(){
returnthis.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是函数做特殊处理;
varfirst,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||typeofvalue!=="string"||jQuery.support.checkClone||!rchecked.test(value))){
returnthis.each(function(index){
varself=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);
//Keepreferencestoclonedscriptsforlaterrestoration
if(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;
//Reenablescripts
jQuery.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){
//Hopeajaxisavailable...
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){
returnjQuery.access(this,function(value){
returnvalue===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){
returnjQuery.access(this,function(value){...},null,value,arguments.length);
}
如果没有参数表示是取值
if(value===undefined){
returnelem.nodeType===1?
elem.innerHTML.replace(rinlinejQuery,""):
undefined;
}
否则看是否能用innerHTML添加内容。点击参考兼容问题
//看看我们是否可以走了一条捷径,只需使用的innerHTML
//需要执行的代码script|style|link等不能使用innerHTML
//htmlSerialize:确保link节点能使用innerHTML正确序列化,这就需要在IE浏览器的包装元素
//leadingWhitespace:IEstrips使用.innerHTML需要以空白开头
//不是需要额外添加结束标签或外围包装标签的元素
if(typeofvalue==="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)){
returnthis.each(function(i){
jQuery(this).wrapAll(html.call(this,i));
});
}
创建包裹层
//获得包裹标签Theelementstowrapthetargetaround
varwrap=jQuery(html,this[0].ownerDocument).eq(0).clone(true);
if(this[0].parentNode){
wrap.insertBefore(this[0]);
}
用包裹裹住当前jQuery对象
wrap.map(function(){
varelem=this;
while(elem.firstChild&&elem.firstChild.nodeType===1){
elem=elem.firstChild;
}
returnelem;
}).append(this);
注意:当前jQuery对象匹配的元素最好只有一个,如果有多个的话不推荐使用,这种情况慎用,后面举例可以看到。
简单的例子,原DOM为(后面都使用这个例子)
<divid='center'class="center">
<divid='ss'class="center">
<inputtype='submit'id='left'class="left">
</div>
</div>
<divclass="right">我是right</div>
$('#center').wrapAll("<p></p>")后,dom变成了
<p>
<divid="center"class="center">
<divid="ss"class="center">
<inputtype="submit"id="left"class="left">
</div>
</div>
</p>
<divclass="right">我是right</div>
慎用:如果当前jQuery所匹配的元素不止一个,例如原DOM执行$('div').wrapAll(“<p></p>”)后结果DOM变成
<p> <divid="center"class="center"></div> <divid="ss"class="center"> <inputtype="submit"id="left"class="left"> </div> <divclass="right">我是right</div> </p>
看到结果了吧,本来#center是#ss的父节点,结果变成了#ss的兄弟节点。
jQuery.fn.wrapInner(在每个匹配元素的所有子节点外部包裹指定的HTML结构)
处理步骤:
传入参数是函数则将函数结果传入
if(jQuery.isFunction(html)){
returnthis.each(function(i){
jQuery(this).wrapInner(html.call(this,i));
});
}
遍历jQuery对象数组,获取每个元素包含的内容(所有子节点)contents,然后使用warpAll包裹住contents
returnthis.each(function(){
varself=jQuery(this),
contents=self.contents();
if(contents.length){
contents.wrapAll(html);
}else{
self.append(html);
}
});
还是使用上面的例子中的原DOM,执行$('div').wrapInner('<p></p>')后结果DOM变成
<divid="center"class="center"> <p> <divid="ss"class="center"> <p> <inputtype="submit"id="left"class="left"> </p> </div> </p> </div> <divclass="right"> <p> 我是right </p> </div>
jQuery.fn.wrap(在每个匹配元素外部包裹指定的HTML结构)
对jQuery的每个元素分别使用wrapAll包裹一下
returnthis.each(function(i){
jQuery(this).wrapAll(isFunction?html.call(this,i):html);
});
行$('div').wrap('<p></p>')后结果DOM变成
<p> <divid="center"class="center"> <p> <divid="ss"class="center"> <inputtype="submit"id="left"class="left"> </div> </p> </div> </p> <p> <divclass="right">我是right</div> </p>
jQuery.fn.unwrap(移除每个匹配元素的父元素)
使用replaceWith用匹配元素父节点的所有子节点替换匹配元素的父节点。当然了父节点是body/html/document肯定是移除不了的
returnthis.parent().each(function(){
if(!jQuery.nodeName(this,"body")){
jQuery(this).replaceWith(this.childNodes);
}
}).end();
执行$('div').wrap()后结果DOM变成
<divid="ss"class="center">
<inputtype="submit"id="left"class="left">
</div>
<divclass="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(typeindata.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的时候,移除cache
if(cache[id]){
deletecache[id];
//IE不允许从节点使用delete删除expando特征,
//也能对文件节点使用removeAttribute函数;
//我们必须处理所有这些情况下,
if(deleteExpando){
deleteelem[internalKey];
}elseif(typeofelem.removeAttribute!==core_strundefined){
elem.removeAttribute(internalKey);
}else{
elem[internalKey]=null;
}
core_deletedIds.push(id);
}
jQuery.fn.detach
detach:function(selector){
returnthis.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;
}
}