原创|其它|编辑:郝浩|2009-12-14 10:14:57.000|阅读 930 次
概述:最近发现各大类库都能利用div.innerHTML=HTML片断来生成节点元素,再把它们插入到目标元素的各个位置上。这东西实际上就是insertAdjacentHTML,但是IE可恶的innerHTML把这优势变成劣势。首先innerHTML会把里面的某些位置的空白去掉,见下面运行框的结果:
# 界面/图表报表/文档/IDE等千款热门软控件火热销售中 >>
最近发现各大类库都能利用div.innerHTML=HTML片断来生成节点元素,再把它们插入到目标元素的各个位置上。这东西实际上就是insertAdjacentHTML,但是IE可恶的innerHTML把这优势变成劣势。首先innerHTML会把里面的某些位置的空白去掉,见下面运行框的结果:
另一个可恶的地方是,在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的工作流程吧。
001.
append:
function
() {
002.
//传入arguments对象,true为要对表格进行特殊处理,回调函数
003.
return
this
.domManip(arguments,
true
,
function
(elem){
004.
if
(
this
.nodeType == 1)
005.
this
.appendChild( elem );
006.
});
007.
},
008.
domManip:
function
( args, table, callback ) {
009.
if
(
this
[0] ) {
//如果存在元素节点
010.
var
fragment = (
this
[0].ownerDocument ||
this
[0]).createDocumentFragment(),
011.
//注意这里是传入三个参数
012.
scripts = jQuery.clean( args, (
this
[0].ownerDocument ||
this
[0]), fragment ),
013.
first = fragment.firstChild;
014.
if
( first )
015.
for
(
var
i = 0, l =
this
.length; i < l; i++ )
016.
callback.call( root(
this
[i], first),
this
.length > 1 || i > 0 ?
017.
fragment.cloneNode(
true
) : fragment );
018.
if
( scripts )
019.
jQuery.each( scripts, evalScript );
020.
}
021.
return
this
;
022.
function
root( elem, cur ) {
023.
return
table && jQuery.nodeName(elem,
"table"
) && jQuery.nodeName(cur,
"tr"
) ?
024.
(elem.getElementsByTagName(
"tbody"
)[0] ||
025.
elem.appendChild(elem.ownerDocument.createElement(
"tbody"
))) :
026.
elem;
027.
}
028.
}
029.
//elems为arguments对象,context为document对象,fragment为空的文档碎片
030.
clean:
function
( elems, context, fragment ) {
031.
context = context || document;
032.
// !context.createElement fails in IE with an error but returns typeof 'object'
033.
if
(
typeof
context.createElement ===
"undefined"
)
034.
//确保context为文档对象
035.
context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
036.
// If a single string is passed in and it's a single tag
037.
// just do a createElement and skip the rest
038.
//如果文档对象里面只有一个标签,如<div>
039.
//我们大概可能是在外面这样调用它$(this).append("<div>")
040.
//这时就直接把它里面的元素名取出来,用document.createElement("div")创建后放进数组返回
041.
if
( !fragment && elems.length === 1 &&
typeof
elems[0] ===
"string"
) {
042.
var
match = /^<(\w+)\s*\/?>$/.exec(elems[0]);
043.
if
( match )
044.
return
[ context.createElement( match[1] ) ];
045.
}
046.
//利用一个div的innerHTML创建众节点
047.
var
ret = [], scripts = [], div = context.createElement(
"div"
);
048.
//如果我们是在外面这样添加$(this).append("<td>表格1</td>","<td>表格1</td>","<td>表格1</td>")
049.
//jQuery.each按它的第四种支分方式(没有参数,有length)遍历aguments对象,callback.call( value, i, value )
050.
jQuery.each(elems,
function
(i, elem){
//i为索引,elem为arguments对象里的元素
051.
if
(
typeof
elem ===
"number"
)
052.
elem +=
''
;
053.
if
( !elem )
054.
return
;
055.
// Convert html string into DOM nodes
056.
if
(
typeof
elem ===
"string"
) {
057.
// Fix "XHTML"-style tags in all browsers
058.
elem = elem.replace(/(<(\w+)[^>]*?)\/>/g,
function
(all, front, tag){
059.
return
tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ?
060.
all :
061.
front +
"></"
+ tag +
">"
;
062.
});
063.
// Trim whitespace, otherwise indexOf won't work as expected
064.
var
tags = elem.replace(/^\s+/,
""
).substring(0, 10).toLowerCase();
065.
var
wrap =
066.
// option or optgroup
067.
!tags.indexOf(
"<opt"
) &&
068.
[ 1,
"<select multiple='multiple'>"
,
"</select>"
] ||
069.
!tags.indexOf(
"<leg"
) &&
070.
[ 1,
"<fieldset>"
,
"</fieldset>"
] ||
071.
tags.match(/^<(thead|tbody|tfoot|colg|cap)/) &&
072.
[ 1,
"<table>"
,
"</table>"
] ||
073.
!tags.indexOf(
"<tr"
) &&
074.
[ 2,
"<table><tbody>"
,
"</tbody></table>"
] ||
075.
// <thead> matched above
076.
(!tags.indexOf(
"<td"
) || !tags.indexOf(
"<th"
)) &&
077.
[ 3,
"<table><tbody><tr>"
,
"</tr></tbody></table>"
] ||
078.
!tags.indexOf(
"<col"
) &&
079.
[ 2,
"<table><tbody></tbody><colgroup>"
,
"</colgroup></table>"
] ||
080.
// IE can't serialize <link> and <script> tags normally
081.
!jQuery.support.htmlSerialize &&
//用于创建link元素
082.
[ 1,
"div<div>"
,
"</div>"
] ||
083.
[ 0,
""
,
""
];
084.
// Go to html and back, then peel off extra wrappers
085.
div.innerHTML = wrap[1] + elem + wrap[2];
//比如"<table><tbody><tr>" +<td>表格1</td>+"</tr></tbody></table>"
086.
// Move to the right depth
087.
while
( wrap[0]-- )
088.
div = div.lastChild;
089.
//处理IE自动插入tbody,如我们使用$('<thead></thead>')创建HTML片断,它应该返回
090.
//'<thead></thead>',而IE会返回'<thead></thead><tbody></tbody>'
091.
if
( !jQuery.support.tbody ) {
092.
// String was a <table>, *may* have spurious <tbody>
093.
var
hasBody = /<tbody/i.test(elem),
094.
tbody = !tags.indexOf(
"<table"
) && !hasBody ?
095.
div.firstChild && div.firstChild.childNodes :
096.
// String was a bare <thead> or <tfoot>
097.
wrap[1] ==
"<table>"
&& !hasBody ?
098.
div.childNodes :
099.
[];
100.
for
(
var
j = tbody.length - 1; j >= 0 ; --j )
101.
//如果是自动插入的里面肯定没有内容
102.
if
( jQuery.nodeName( tbody[ j ],
"tbody"
) && !tbody[ j ].childNodes.length )
103.
tbody[ j ].parentNode.removeChild( tbody[ j ] );
104.
}
105.
// IE completely kills leading whitespace when innerHTML is used
106.
if
( !jQuery.support.leadingWhitespace && /^\s/.test( elem ) )
107.
div.insertBefore( context.createTextNode( elem.match(/^\s*/)[0] ), div.firstChild );
108.
//把所有节点做成纯数组
109.
elem = jQuery.makeArray( div.childNodes );
110.
}
111.
if
( elem.nodeType )
112.
ret.push( elem );
113.
else
114.
//全并两个数组,merge方法会处理IE下object元素下消失了的param元素
115.
ret = jQuery.merge( ret, elem );
116.
});
117.
if
( fragment ) {
118.
for
(
var
i = 0; ret[i]; i++ ) {
119.
//如果第一层的childNodes就有script元素节点,就用scripts把它们收集起来,供后面用globalEval动态执行
120.
if
( jQuery.nodeName( ret[i],
"script"
) && (!ret[i].type || ret[i].type.toLowerCase() ===
"text/javascript"
) ) {
121.
scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
122.
}
else
{
123.
//遍历各层节点,收集script元素节点
124.
if
( ret[i].nodeType === 1 )
125.
ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName(
"script"
))) );
126.
fragment.appendChild( ret[i] );
127.
}
128.
}
129.
return
scripts;
//由于动态插入是传入三个参数,因此这里就返回了
130.
}
131.
return
ret;
132.
},
真是复杂的让人掉眼泪!不过jQuery的实现并不太高明,它把插入的东西统统用clean转换为节点集合,再把它们放到一个文档碎片中,然后用appendChild与insertBefore插入它们。在除了火狐外,其他浏览器都支持insertAdjactentXXX家族的今日,应该好好利用这些原生API。下面是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、
01.
02.
(
function
() {
03.
if
(
'HTMLElement'
in
this
) {
04.
if
(
'insertAdjacentHTML'
in
HTMLElement.prototype) {
05.
return
06.
}
07.
}
else
{
08.
return
09.
}
10.
11.
function
insert(w, n) {
12.
switch
(w.toUpperCase()) {
13.
case
'BEFOREEND'
:
14.
this
.appendChild(n)
15.
break
16.
case
'BEFOREBEGIN'
:
17.
this
.parentNode.insertBefore(n,
this
)
18.
break
19.
case
'AFTERBEGIN'
:
20.
this
.insertBefore(n,
this
.childNodes[0])
21.
break
22.
case
'AFTEREND'
:
23.
this
.parentNode.insertBefore(n,
this
.nextSibling)
24.
break
25.
}
26.
}
27.
28.
function
insertAdjacentText(w, t) {
29.
insert.call(
this
, w, document.createTextNode(t ||
''
))
30.
}
31.
32.
function
insertAdjacentHTML(w, h) {
33.
var
r = document.createRange()
34.
r.selectNode(
this
)
35.
insert.call(
this
, w, r.createContextualFragment(h))
36.
}
37.
38.
function
insertAdjacentElement(w, n) {
39.
insert.call(
this
, w, n)
40.
return
n
41.
}
42.
43.
HTMLElement.prototype.insertAdjacentText = insertAdjacentText
44.
HTMLElement.prototype.insertAdjacentHTML = insertAdjacentHTML
45.
HTMLElement.prototype.insertAdjacentElement = insertAdjacentElement
46.
})()
我们可以利用它设计出更快更合理的动态插入方法。下面是我的一些实现:
01.
02.
//四个插入方法,对应insertAdjactentHTML的四个插入位置,名字就套用jQuery的
03.
//stuff可以为字符串,各种节点或dom对象(一个类数组对象,便于链式操作!)
04.
//代码比jQuery的实现简洁漂亮吧!
05.
append:
function
(stuff){
06.
return
dom.batch(
this
,
function
(el){
07.
dom.insert(el,stuff,
"beforeEnd"
);
08.
});
09.
},
10.
prepend:
function
(stuff){
11.
return
dom.batch(
this
,
function
(el){
12.
dom.insert(el,stuff,
"afterBegin"
);
13.
});
14.
},
15.
before:
function
(stuff){
16.
return
dom.batch(
this
,
function
(el){
17.
dom.insert(el,stuff,
"beforeBegin"
);
18.
});
19.
},
20.
after:
function
(stuff){
21.
return
dom.batch(
this
,
function
(el){
22.
dom.insert(el,stuff,
"afterEnd"
);
23.
});
24.
}
它们里面都是调用了两个静态方法,batch与insert。由于dom对象是类数组对象,我仿效jQuery那样为它实现了几个重要迭代器,forEach、map与filter等。一个dom对象包含复数个DOM元素,我们就可以用forEach遍历它们,执行其中的回调方法。
1.
batch:
function
(els,callback){
2.
els.forEach(callback);
3.
return
els;
//链式操作
4.
},
insert方法执行jQuery的domManip方法相应的机能(dojo则为place方法),但insert方法每次处理一个元素节点,不像jQuery那样处理一组元素节点。群集处理已经由上面batch方法分离出去了。
01.
insert :
function
(el,stuff,where){
02.
//定义两个全局的东西,提供内部方法调用
03.
var
doc = el.ownerDocument || dom.doc,
04.
fragment = doc.createDocumentFragment();
05.
if
(stuff.version){
//如果是dom对象,则把它里面的元素节点移到文档碎片中
06.
stuff.forEach(
function
(el){
07.
fragment.appendChild(el);
08.
})
09.
stuff = fragment;
10.
}
11.
//供火狐与IE部分元素调用
12.
dom._insertAdjacentElement =
function
(el,node,where){
13.
switch
(where){
14.
case
'beforeBegin'
:
15.
el.parentNode.insertBefore(node,el)
16.
break
;
17.
case
'afterBegin'
:
18.
el.insertBefore(node,el.firstChild);
19.
break
;
20.
case
'beforeEnd'
:
21.
el.appendChild(node);
22.
break
;
23.
case
'afterEnd'
:
24.
if
(el.nextSibling) el.parentNode.insertBefore(node,el.nextSibling);
25.
else
el.parentNode.appendChild(node);
26.
break
;
27.
}
28.
};
29.
//供火狐调用
30.
dom._insertAdjacentHTML =
function
(el,htmlStr,where){
31.
var
range = doc.createRange();
32.
switch
(where) {
33.
case
"beforeBegin"
:
//before
34.
range.setStartBefore(el);
35.
break
;
36.
case
"afterBegin"
:
//after
37.
range.selectNodeContents(el);
38.
range.collapse(
true
);
39.
break
;
40.
case
"beforeEnd"
:
//append
41.
range.selectNodeContents(el);
42.
range.collapse(
false
);
43.
break
;
44.
case
"afterEnd"
:
//prepend
45.
range.setStartAfter(el);
46.
break
;
47.
}
48.
var
parsedHTML = range.createContextualFragment(htmlStr);
49.
dom._insertAdjacentElement(el,parsedHTML,where);
50.
};
51.
//以下元素的innerHTML在IE中是只读的,调用insertAdjacentElement进行插入就会出错
52.
// col, colgroup, frameset, html, head, style, title,table, tbody, tfoot, thead, 与tr;
53.
dom._insertAdjacentIEFix =
function
(el,htmlStr,where){
54.
var
parsedHTML = dom.parseHTML(htmlStr,fragment);
55.
dom._insertAdjacentElement(el,parsedHTML,where)
56.
};
57.
//如果是节点则复制一份
58.
stuff = stuff.nodeType ? stuff.cloneNode(
true
) : stuff;
59.
if
(el.insertAdjacentHTML) {
//ie,chrome,opera,safari都已实现insertAdjactentXXX家族
60.
try
{
//适合用于opera,safari,chrome与IE
61.
el[
'insertAdjacent'
+ (stuff.nodeType ?
'Element'
:
'HTML'
)](where,stuff);
62.
}
catch
(e){
63.
//IE的某些元素调用insertAdjacentXXX可能出错,因此使用此补丁
64.
dom._insertAdjacentIEFix(el,stuff,where);
65.
}
66.
}
else
{
67.
//火狐专用
68.
dom[
'_insertAdjacent'
+ (stuff.nodeType ?
'Element'
:
'HTML'
)](el,stuff,where);
69.
}
70.
}
insert方法在实现火狐插入操作中,使用了W3C DOM Range对象的一些罕见方法,具体可到火狐官网查看。下面实现把字符串转换为节点,利用innerHTML这个伟大的方法。Prototype.js称之为_getContentFromAnonymousElement,但有许多问题,dojo称之为_toDom,mootools的Element.Properties.html,jQuery的clean。Ext没有这东西,它只支持传入HTML片断的insertAdjacentHTML方法,不支持传入元素节点的insertAdjacentElement。但有时,我们需要插入文本节点(并不包裹于元素节点之中),这时我们就需要用文档碎片做容器了,insert方法出场了。
01.
parseHTML :
function
(htmlStr, fragment){
02.
var
div = dom.doc.createElement(
"div"
),
03.
reSingleTag = /^<(\w+)\s*\/?>$/;
//匹配单个标签,如<li>
04.
htmlStr +=
''
;
05.
if
(reSingleTag.test(htmlStr)){
//如果str为单个标签
06.
return
[dom.doc.createElement(RegExp.$1)]
07.
}
08.
var
tagWrap = {
09.
option: [
"select"
],
10.
optgroup: [
"select"
],
11.
tbody: [
"table"
],
12.
thead: [
"table"
],
13.
tfoot: [
"table"
],
14.
tr: [
"table"
,
"tbody"
],
15.
td: [
"table"
,
"tbody"
,
"tr"
],
16.
th: [
"table"
,
"thead"
,
"tr"
],
17.
legend: [
"fieldset"
],
18.
caption: [
"table"
],
19.
colgroup: [
"table"
],
20.
col: [
"table"
,
"colgroup"
],
21.
li: [
"ul"
],
22.
link:[
"div"
]
23.
};
24.
for
(
var
param
in
tagWrap){
25.
var
tw = tagWrap[param];
26.
switch
(param) {
27.
case
"option"
:tw.pre =
'<select multiple="multiple">'
;
break
;
28.
case
"link"
: tw.pre =
'fixbug<div>'
;
break
;
29.
default
: tw.pre =
"<"
+ tw.join(
"><"
) +
">"
;
30.
}
31.
tw.post =
"</"
+ tw.reverse().join(
"></"
) +
">"
;
32.
}
33.
var
reMultiTag = /<\s*([\w\:]+)/,
//匹配一对标签或多个标签,如<li></li>,li
34.
match = htmlStr.match(reMultiTag),
35.
tag = match ? match[1].toLowerCase() :
""
;
//解析为<li,li
36.
if
(match && tagWrap[tag]){
37.
var
wrap = tagWrap[tag];
38.
div.innerHTML = wrap.pre + htmlStr + wrap.post;
39.
n = wrap.length;
40.
while
(--n >= 0)
//返回我们已经添加的内容
41.
div = div.lastChild;
42.
}
else
{
43.
div.innerHTML = htmlStr;
44.
}
45.
//处理IE自动插入tbody,如我们使用dom.parseHTML('<thead></thead>')转换HTML片断,它应该返回
46.
//'<thead></thead>',而IE会返回'<thead></thead><tbody></tbody>'
47.
//亦即,在标准浏览器中return div.children.length会返回1,IE会返回2
48.
if
(dom.feature.autoInsertTbody && !!tagWrap[tag]){
49.
var
ownInsert = tagWrap[tag].join(
''
).indexOf(
"tbody"
) !== -1,
//我们插入的
50.
tbody = div.getElementsByTagName(
"tbody"
),
51.
autoInsert = tbody.length > 0;
//IE插入的
52.
if
(!ownInsert && autoInsert){
53.
for
(
var
i=0,n=tbody.length;i<n;i++){
54.
if
(!tbody[i].childNodes.length )
//如果是自动插入的里面肯定没有内容
55.
tbody[i].parentNode.removeChild( tbody[i] );
56.
}
57.
}
58.
}
59.
if
(dom.feature.autoRemoveBlank && /^\s/.test(htmlStr) )
60.
div.insertBefore( dom.doc.createTextNode(htmlStr.match(/^\s*/)[0] ), div.firstChild );
61.
if
(fragment) {
62.
var
firstChild;
63.
while
((firstChild = div.firstChild)){
// 将div上的节点转移到文档碎片上!
64.
fragment.appendChild(firstChild);
65.
}
66.
return
fragment;
67.
}
68.
return
div.children;
69.
}
嘛,基本上就是这样,运行起来比jQuery快许多,代码实现也算优美,至少没有像jQuery那样乱成一团。jQuery还有四个反转方法。下面是jQuery的实现:
01.
02.
jQuery.each({
03.
appendTo:
"append"
,
04.
prependTo:
"prepend"
,
05.
insertBefore:
"before"
,
06.
insertAfter:
"after"
,
07.
replaceAll:
"replaceWith"
08.
},
function
(name, original){
09.
jQuery.fn[ name ] =
function
( selector ) {
//插入物(html,元素节点,jQuery对象)
10.
var
ret = [], insert = jQuery( selector );
//将插入转变为jQuery对象
11.
for
(
var
i = 0, l = insert.length; i < l; i++ ) {
12.
var
elems = (i > 0 ?
this
.clone(
true
) :
this
).get();
13.
jQuery.fn[ original ].apply( jQuery(insert[i]), elems );
//调用四个已实现的插入方法
14.
ret = ret.concat( elems );
15.
}
16.
return
this
.pushStack( ret, name, selector );
//由于没有把链式操作的代码分离出去,需要自行实现
17.
};
18.
});
我的实现:
01.
02.
dom.each({
03.
appendTo:
'append'
,
04.
prependTo:
'prepend'
,
05.
insertBefore:
'before'
,
06.
insertAfter:
'after'
07.
},
function
(method,name){
08.
dom.prototype[name] =
function
(stuff){
09.
return
dom(stuff)[method](
this
);
10.
};
11.
});
大致的代码都给出,大家可以各取所需。
本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@evget.com
文章转载自:博客园