javascript 动态插入技术(2)
作者:司徒正美 来源:Ruby's Louvre 时间:2009-12-14 20:50:00
下面是Ext利用insertAdjactentHTML等方法实现的DomHelper方法,官网给出的数据:
Insertion Method | IE7 beta 2 | IE6 | FF 1.5 | Opera 9 |
---|---|---|---|---|
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' in this) {
if('insertAdjacentHTML' in HTMLElement.prototype) {
return
}
} else {
return
}
function insert(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
}
}
function insertAdjacentText(w, t) {
insert.call(this, w, document.createTextNode(t || ''))
}
function insertAdjacentHTML(w, h) {
var r = document.createRange()
r.selectNode(this)
insert.call(this, w, r.createContextualFragment(h))
}
function insertAdjacentElement(w, n) {
insert.call(this, w, n)
return n
}
HTMLElement.prototype.insertAdjacentText = insertAdjacentText
HTMLElement.prototype.insertAdjacentHTML = insertAdjacentHTML
HTMLElement.prototype.insertAdjacentElement = insertAdjacentElement
})()
我们可以利用它设计出更快更合理的动态插入方法。下面是我的一些实现:
//四个插入方法,对应insertAdjactentHTML的四个插入位置,名字就套用jQuery的
//stuff可以为字符串,各种节点或dom对象(一个类数组对象,便于链式操作!)
//代码比jQuery的实现简洁漂亮吧!
append:function(stuff){
return dom.batch(this,function(el){
dom.insert(el,stuff,"beforeEnd");
});
},
prepend:function(stuff){
return dom.batch(this,function(el){
dom.insert(el,stuff,"afterBegin");
});
},
before:function(stuff){
return dom.batch(this,function(el){
dom.insert(el,stuff,"beforeBegin");
});
},
after:function(stuff){
return dom.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);
return els;//链式操作
},
insert方法执行jQuery的domManip方法相应的机能(dojo则为place方法),但insert方法每次处理一个元素节点,不像jQuery那样处理一组元素节点。群集处理已经由上面batch方法分离出去了。
insert : function(el,stuff,where){
//定义两个全局的东西,提供内部方法调用
var doc = 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);
else el.parentNode.appendChild(node);
break;
}
};
//供火狐调用
dom._insertAdjacentHTML = function(el,htmlStr,where){
var range = 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;
}
var parsedHTML = 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){
var parsedHTML = 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){
var div = dom.doc.createElement("div"),
reSingleTag = /^<(\w+)\s*\/?>$/;//匹配单个标签,如<li>
htmlStr += '';
if(reSingleTag.test(htmlStr)){//如果str为单个标签
return [dom.doc.createElement(RegExp.$1)]
}
var tagWrap = {
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(var param in tagWrap){
var tw = tagWrap[param];
switch (param) {
case "option":tw.pre = '<select multiple="multiple">'; break;
case "link": tw.pre = 'fixbug<div>'; break;
default : tw.pre = "<" + tw.join("><") + ">";
}
tw.post = "</" + tw.reverse().join("></") + ">";
}
var reMultiTag = /<\s*([\w\:]+)/,//匹配一对标签或多个标签,如<li></li>,li
match = htmlStr.match(reMultiTag),
tag = match ? match[1].toLowerCase() : "";//解析为<li,li
if(match && tagWrap[tag]){
var wrap = 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>'
//亦即,在标准浏览器中return div.children.length会返回1,IE会返回2
if(dom.feature.autoInsertTbody && !!tagWrap[tag]){
var ownInsert = tagWrap[tag].join('').indexOf("tbody") !== -1,//我们插入的
tbody = div.getElementsByTagName("tbody"),
autoInsert = tbody.length > 0;//IE插入的
if(!ownInsert && autoInsert){
for(var i=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) {
var firstChild;
while((firstChild = div.firstChild)){ // 将div上的节点转移到文档碎片上!
fragment.appendChild(firstChild);
}
return fragment;
}
return div.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对象)
var ret = [], insert = jQuery( selector );//将插入转变为jQuery对象
for ( var i = 0, l = insert.length; i < l; i++ ) {
var elems = (i > 0 ? this.clone(true) : this).get();
jQuery.fn[ original ].apply( jQuery(insert[i]), elems );//调用四个已实现的插入方法
ret = ret.concat( elems );
}
return this.pushStack( ret, name, selector );//由于没有把链式操作的代码分离出去,需要自行实现
};
});
我的实现:
dom.each({
appendTo: 'append',
prependTo: 'prepend',
insertBefore: 'before',
insertAfter: 'after'
},function(method,name){
dom.prototype[name] = function(stuff){
return dom(stuff)[method](this);
};
});
大致的代码都给出,大家可以各取所需。