zwq 2011-08-19
最近发现各大类库都能利用div.innerHTML=HTML片断来生成节点元素,再把它们插入到目标元素的各个位置上。这东西实际上就是insertAdjacentHTML,但是IE可恶的innerHTML把这优势变成劣势。首先innerHTML会把里面的某些位置的空白去掉,见下面运行框的结果:(复制运行)
<!doctype html>
<htmldir="ltr"lang="zh-CN">
<head>
<metacharset="utf-8"/>
<title>
IE的innerHTMLBy司徒正美
</title>
<scripttype="text/javascript">
window.onload=function(){
vardiv=document.createElement("div");
div.innerHTML="<td><b>司徒</b>正美</td>"
alert("|"+div.innerHTML+"|");
varc=div.childNodes;
alert("生成的节点个数"+c.length);
for(vari=0,n=c.length;i<n;i++){
alert(c[i].nodeType);
if(c[i].nodeType===1){
alert("::"+c[i].childNodes.length);
}
}
}
</script>
</head>
<body>
<pid="p">
</p>
</body>
</html>另一个可恶的地方是,在IE中以下元素的innerHTML是只读的:col、 colgroup、frameset、html、 head、style、table、tbody、 tfoot、 thead、title 与 tr。为了收拾它们,Ext特意弄了个insertIntoTable。insertIntoTable就是利用DOM的insertBefore与appendChild来添加,情况基本同jQuery。不过jQuery是完全依赖这两个方法,Ext还使用了insertAdjacentHTML。为了提高效率,所有类库都不约而同地使用了文档碎片。基本流程都是通过div.innerHTML提取出节点,然后转移到文档碎片上,然后用insertBefore与appendChild插入节点。对于火狐,Ext还使用了createContextualFragment解析文本,直接插入其目标位置上。显然,Ext的比jQuery是快许多的。不过jQuery的插入的不单是HTML片断,还有各种节点与jQuery对象。下面重温一下jQuery的工作流程吧。
append: function() {
//传入arguments对象,true为要对表格进行特殊处理,回调函数
returnthis.domManip(arguments,true,function(elem){
if(this.nodeType==1)
this.appendChild(elem);
});
},
domManip:function(args,table,callback){
if(this[0]){//如果存在元素节点
varfragment=(this[0].ownerDocument||this[0]).createDocumentFragment(),
//注意这里是传入三个参数
scripts=jQuery.clean(args,(this[0].ownerDocument||this[0]),fragment),
first=fragment.firstChild;
if(first)
for(vari=0,l=this.length;i<l;i++)
callback.call(root(this[i],first),this.length>1||i>0?
fragment.cloneNode(true):fragment);
if(scripts)
jQuery.each(scripts,evalScript);
}
returnthis;
functionroot(elem,cur){
returntable&&jQuery.nodeName(elem,"table")&&jQuery.nodeName(cur,"tr")?
(elem.getElementsByTagName("tbody")[0]||
elem.appendChild(elem.ownerDocument.createElement("tbody"))):
elem;
}
}
//elems为arguments对象,context为document对象,fragment为空的文档碎片
clean:function(elems,context,fragment){
context=context||document;
//!context.createElementfailsinIEwithanerrorbutreturnstypeof'object'
if(typeofcontext.createElement==="undefined")
//确保context为文档对象
context=context.ownerDocument||context[0]&&context[0].ownerDocument||document;
//Ifasinglestringispassedinandit'sasingletag
//justdoacreateElementandskiptherest
//如果文档对象里面只有一个标签,如<div>
//我们大概可能是在外面这样调用它$(this).append("<div>")
//这时就直接把它里面的元素名取出来,用document.createElement("div")创建后放进数组返回
if(!fragment&&elems.length===1&&typeofelems[0]==="string"){
varmatch=/^<(\w+)\s*\/?>$/.exec(elems[0]);
if(match)
return[context.createElement(match[1])];
}
//利用一个div的innerHTML创建众节点
varret=[],scripts=[],div=context.createElement("div");
//如果我们是在外面这样添加$(this).append("<td>表格1</td>","<td>表格1</td>","<td>表格1</td>")
//jQuery.each按它的第四种支分方式(没有参数,有length)遍历aguments对象,callback.call(value,i,value)
jQuery.each(elems,function(i,elem){//i为索引,elem为arguments对象里的元素
if(typeofelem==="number")
elem+='';
if(!elem)
return;
//ConverthtmlstringintoDOMnodes
if(typeofelem==="string"){
//Fix"XHTML"-styletagsinallbrowsers
elem=elem.replace(/(<(\w+)[^>]*?)\/>/g,function(all,front,tag){
returntag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?
all:
front+"></"+tag+">";
});
//Trimwhitespace,otherwiseindexOfwon'tworkasexpected
vartags=elem.replace(/^\s+/,"").substring(0,10).toLowerCase();
varwrap=
//optionoroptgroup
!tags.indexOf("<opt")&&
[1,"<selectmultiple='multiple'>","</select>"]||
!tags.indexOf("<leg")&&
[1,"<fieldset>","</fieldset>"]||
tags.match(/^<(thead|tbody|tfoot|colg|cap)/)&&
[1,"<table>","</table>"]||
!tags.indexOf("<tr")&&
[2,"<table><tbody>","</tbody></table>"]||
//<thead>matchedabove
(!tags.indexOf("<td")||!tags.indexOf("<th"))&&
[3,"<table><tbody><tr>","</tr></tbody></table>"]||
!tags.indexOf("<col")&&
[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]||
//IEcan'tserialize<link>and<script>tagsnormally
!jQuery.support.htmlSerialize&&//用于创建link元素
[1,"div<div>","</div>"]||
[0,"",""];
//Gotohtmlandback,thenpeeloffextrawrappers
div.innerHTML=wrap[1]+elem+wrap[2];//比如"<table><tbody><tr>"+<td>表格1</td>+"</tr></tbody></table>"
//Movetotherightdepth
while(wrap[0]--)
div=div.lastChild;
//处理IE自动插入tbody,如我们使用$('<thead></thead>')创建HTML片断,它应该返回
//'<thead></thead>',而IE会返回'<thead></thead><tbody></tbody>'
if(!jQuery.support.tbody){
//Stringwasa<table>,*may*havespurious<tbody>
varhasBody=/<tbody/i.test(elem),
tbody=!tags.indexOf("<table")&&!hasBody?
div.firstChild&&div.firstChild.childNodes:
//Stringwasabare<thead>or<tfoot>
wrap[1]=="<table>"&&!hasBody?
div.childNodes:
[];
for(varj=tbody.length-1;j>=0;--j)
//如果是自动插入的里面肯定没有内容
if(jQuery.nodeName(tbody[j],"tbody")&&!tbody[j].childNodes.length)
tbody[j].parentNode.removeChild(tbody[j]);
}
//IEcompletelykillsleadingwhitespacewheninnerHTMLisused
if(!jQuery.support.leadingWhitespace&&/^\s/.test(elem))
div.insertBefore(context.createTextNode(elem.match(/^\s*/)[0]),div.firstChild);
//把所有节点做成纯数组
elem=jQuery.makeArray(div.childNodes);
}
if(elem.nodeType)
ret.push(elem);
else
//全并两个数组,merge方法会处理IE下object元素下消失了的param元素
ret=jQuery.merge(ret,elem);
});
if(fragment){
for(vari=0;ret[i];i++){
//如果第一层的childNodes就有script元素节点,就用scripts把它们收集起来,供后面用globalEval动态执行
if(jQuery.nodeName(ret[i],"script")&&(!ret[i].type||ret[i].type.toLowerCase()==="text/javascript")){
scripts.push(ret[i].parentNode?ret[i].parentNode.removeChild(ret[i]):ret[i]);
}else{
//遍历各层节点,收集script元素节点
if(ret[i].nodeType===1)
ret.splice.apply(ret,[i+1,0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))));
fragment.appendChild(ret[i]);
}
}
returnscripts;//由于动态插入是传入三个参数,因此这里就返回了
}
returnret;
},
真是复杂的让人掉眼泪!不过jQuery的实现并不太高明,它把插入的东西统统用clean转换为节点集合,再把它们放到一个文档碎片中,然后用appendChild与insertBefore插入它们。在除了火狐外,其他浏览器都支持insertAdjactentXXX家族的今日,应该好好利用这些原生API。下面是Ext利用insertAdjactentHTML等方法实现的DomHelper方法,官网给出的数据:
| DOM | .730 | 1.35 | .420 | .280 |
| HTML Fragments | .360 | .380 | .400 | .260 |
| Template | .320 | .335 | .385 | .220 |
| Compiled Template | .295 | .300 | .350 | .210 |
数据来源:《Tutorial:使用DomHelper 创建元素的DOM、HTML片断和模版》
这数据有点老了,而且最新3.03早就解决了在IE table插入内容的诟病(table,tbody,tr等的innerHTML都是只读,insertAdjactentHTML,pasteHTML等方法都无法修改其内容,要用又慢又标准的DOM方法才行,Ext的早期版本就在这里遭遇滑铁卢了)。可以看出,结合insertAdjactentHTML与文档碎片后,IE6插入节点的速度也得到难以置信的提升,直逼火狐。基于它,Ext开发了四个分支方法insertBefore、insertAfter、insertFirst、append,分别对应jQuery的before、after、prepend与append。不过,jQuery还把这几个方法巧妙地调换了调用者与传入参数,衍生出insertBefore、insertAfter、prependTo与appendTo这几个方法。但不管怎么说,jQuery这样一刀切的做法实现令人不敢苛同。下面是在火狐中实现insertAdjactentXXX家族的一个版本:
(function() {
if('HTMLElement'inthis){
if('insertAdjacentHTML'inHTMLElement.prototype){
return
}
}else{
return
}
functioninsert(w,n){
switch(w.toUpperCase()){
case'BEFOREEND':
this.appendChild(n)
break
case'BEFOREBEGIN':
this.parentNode.insertBefore(n,this)
break
case'AFTERBEGIN':
this.insertBefore(n,this.childNodes[0])
break
case'AFTEREND':
this.parentNode.insertBefore(n,this.nextSibling)
break
}
}
functioninsertAdjacentText(w,t){
insert.call(this,w,document.createTextNode(t||''))
}
functioninsertAdjacentHTML(w,h){
varr=document.createRange()
r.selectNode(this)
insert.call(this,w,r.createContextualFragment(h))
}
functioninsertAdjacentElement(w,n){
insert.call(this,w,n)
returnn
}
HTMLElement.prototype.insertAdjacentText=insertAdjacentText
HTMLElement.prototype.insertAdjacentHTML=insertAdjacentHTML
HTMLElement.prototype.insertAdjacentElement=insertAdjacentElement
})()我们可以利用它设计出更快更合理的动态插入方法。下面是我的一些实现:
//四个插入方法,对应insertAdjactentHTML的四个插入位置,名字就套用jQuery的
//stuff可以为字符串,各种节点或dom对象(一个类数组对象,便于链式操作!)
//代码比jQuery的实现简洁漂亮吧!
append:function(stuff){
returndom.batch(this,function(el){
dom.insert(el,stuff,"beforeEnd");
});
},
prepend:function(stuff){
returndom.batch(this,function(el){
dom.insert(el,stuff,"afterBegin");
});
},
before:function(stuff){
returndom.batch(this,function(el){
dom.insert(el,stuff,"beforeBegin");
});
},
after:function(stuff){
returndom.batch(this,function(el){
dom.insert(el,stuff,"afterEnd");
});
}它们里面都是调用了两个静态方法,batch与insert。由于dom对象是类数组对象,我仿效jQuery那样为它实现了几个重要迭代器,forEach、map与filter等。一个dom对象包含复数个DOM元素,我们就可以用forEach遍历它们,执行其中的回调方法。
batch:function(els,callback){
els.forEach(callback);
returnels;//链式操作
},insert方法执行jQuery的domManip方法相应的机能(dojo则为place方法),但insert方法每次处理一个元素节点,不像jQuery那样处理一组元素节点。群集处理已经由上面batch方法分离出去了。
insert : function(el,stuff,where){
//定义两个全局的东西,提供内部方法调用
vardoc=el.ownerDocument||dom.doc,
fragment=doc.createDocumentFragment();
if(stuff.version){//如果是dom对象,则把它里面的元素节点移到文档碎片中
stuff.forEach(function(el){
fragment.appendChild(el);
})
stuff=fragment;
}
//供火狐与IE部分元素调用
dom._insertAdjacentElement=function(el,node,where){
switch(where){
case'beforeBegin':
el.parentNode.insertBefore(node,el)
break;
case'afterBegin':
el.insertBefore(node,el.firstChild);
break;
case'beforeEnd':
el.appendChild(node);
break;
case'afterEnd':
if(el.nextSibling)el.parentNode.insertBefore(node,el.nextSibling);
elseel.parentNode.appendChild(node);
break;
}
};
//供火狐调用
dom._insertAdjacentHTML=function(el,htmlStr,where){
varrange=doc.createRange();
switch(where){
case"beforeBegin"://before
range.setStartBefore(el);
break;
case"afterBegin"://after
range.selectNodeContents(el);
range.collapse(true);
break;
case"beforeEnd"://append
range.selectNodeContents(el);
range.collapse(false);
break;
case"afterEnd"://prepend
range.setStartAfter(el);
break;
}
varparsedHTML=range.createContextualFragment(htmlStr);
dom._insertAdjacentElement(el,parsedHTML,where);
};
//以下元素的innerHTML在IE中是只读的,调用insertAdjacentElement进行插入就会出错
//col,colgroup,frameset,html,head,style,title,table,tbody,tfoot,thead,与tr;
dom._insertAdjacentIEFix=function(el,htmlStr,where){
varparsedHTML=dom.parseHTML(htmlStr,fragment);
dom._insertAdjacentElement(el,parsedHTML,where)
};
//如果是节点则复制一份
stuff=stuff.nodeType?stuff.cloneNode(true):stuff;
if(el.insertAdjacentHTML){//ie,chrome,opera,safari都已实现insertAdjactentXXX家族
try{//适合用于opera,safari,chrome与IE
el['insertAdjacent'+(stuff.nodeType?'Element':'HTML')](where,stuff);
}catch(e){
//IE的某些元素调用insertAdjacentXXX可能出错,因此使用此补丁
dom._insertAdjacentIEFix(el,stuff,where);
}
}else{
//火狐专用
dom['_insertAdjacent'+(stuff.nodeType?'Element':'HTML')](el,stuff,where);
}
}insert方法在实现火狐插入操作中,使用了W3C DOM Range对象的一些罕见方法,具体可到火狐官网查看。下面实现把字符串转换为节点,利用innerHTML这个伟大的方法。Prototype.js称之为_getContentFromAnonymousElement,但有许多问题,dojo称之为_toDom,mootools的Element.Properties.html,jQuery的clean。Ext没有这东西,它只支持传入HTML片断的insertAdjacentHTML方法,不支持传入元素节点的insertAdjacentElement。但有时,我们需要插入文本节点(并不包裹于元素节点之中),这时我们就需要用文档碎片做容器了,insert方法出场了。
parseHTML : function(htmlStr, fragment){
vardiv=dom.doc.createElement("div"),
reSingleTag=/^<(\w+)\s*\/?>$/;//匹配单个标签,如<li>
htmlStr+='';
if(reSingleTag.test(htmlStr)){//如果str为单个标签
return[dom.doc.createElement(RegExp.$1)]
}
vartagWrap={
option:["select"],
optgroup:["select"],
tbody:["table"],
thead:["table"],
tfoot:["table"],
tr:["table","tbody"],
td:["table","tbody","tr"],
th:["table","thead","tr"],
legend:["fieldset"],
caption:["table"],
colgroup:["table"],
col:["table","colgroup"],
li:["ul"],
link:["div"]
};
for(varparamintagWrap){
vartw=tagWrap[param];
switch(param){
case"option":tw.pre='<selectmultiple="multiple">';break;
case"link":tw.pre='fixbug<div>';break;
default:tw.pre="<"+tw.join("><")+">";
}
tw.post="</"+tw.reverse().join("></")+">";
}
varreMultiTag=/<\s*([\w\:]+)/,//匹配一对标签或多个标签,如<li></li>,li
match=htmlStr.match(reMultiTag),
tag=match?match[1].toLowerCase():"";//解析为<li,li
if(match&&tagWrap[tag]){
varwrap=tagWrap[tag];
div.innerHTML=wrap.pre+htmlStr+wrap.post;
n=wrap.length;
while(--n>=0)//返回我们已经添加的内容
div=div.lastChild;
}else{
div.innerHTML=htmlStr;
}
//处理IE自动插入tbody,如我们使用dom.parseHTML('<thead></thead>')转换HTML片断,它应该返回
//'<thead></thead>',而IE会返回'<thead></thead><tbody></tbody>'
//亦即,在标准浏览器中returndiv.children.length会返回1,IE会返回2
if(dom.feature.autoInsertTbody&&!!tagWrap[tag]){
varownInsert=tagWrap[tag].join('').indexOf("tbody")!==-1,//我们插入的
tbody=div.getElementsByTagName("tbody"),
autoInsert=tbody.length>0;//IE插入的
if(!ownInsert&&autoInsert){
for(vari=0,n=tbody.length;i<n;i++){
if(!tbody[i].childNodes.length)//如果是自动插入的里面肯定没有内容
tbody[i].parentNode.removeChild(tbody[i]);
}
}
}
if(dom.feature.autoRemoveBlank&&/^\s/.test(htmlStr))
div.insertBefore(dom.doc.createTextNode(htmlStr.match(/^\s*/)[0]),div.firstChild);
if(fragment){
varfirstChild;
while((firstChild=div.firstChild)){//将div上的节点转移到文档碎片上!
fragment.appendChild(firstChild);
}
returnfragment;
}
returndiv.children;
}嘛,基本上就是这样,运行起来比jQuery快许多,代码实现也算优美,至少没有像jQuery那样乱成一团。jQuery还有四个反转方法。下面是jQuery的实现:
jQuery.each({
appendTo:"append",
prependTo:"prepend",
insertBefore:"before",
insertAfter:"after",
replaceAll:"replaceWith"
},function(name,original){
jQuery.fn[name]=function(selector){//插入物(html,元素节点,jQuery对象)
varret=[],insert=jQuery(selector);//将插入转变为jQuery对象
for(vari=0,l=insert.length;i<l;i++){
varelems=(i>0?this.clone(true):this).get();
jQuery.fn[original].apply(jQuery(insert[i]),elems);//调用四个已实现的插入方法
ret=ret.concat(elems);
}
returnthis.pushStack(ret,name,selector);//由于没有把链式操作的代码分离出去,需要自行实现
};
});我的实现:
dom.each({
appendTo:'append',
prependTo:'prepend',
insertBefore:'before',
insertAfter:'after'
},function(method,name){
dom.prototype[name]=function(stuff){
returndom(stuff)[method](this);
};
});大致的代码都给出,大家可以各取所需
现在用top.innerHTML="..........";的方法就可以向这个id的位置写入HTML代码了。例如top.innerHTML="<input type="button" name=&qu
JavaScript代码innerHTML='<option>1</option>'的时候测试出来的结果却是"1</option>",ie6 7 8 都存在这个问题。经过测试,发现 在用innerHT