javascript 动态插入技术

原创|其它|编辑:郝浩|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的工作流程吧。

真是复杂的让人掉眼泪!不过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开发了四个分支方法insertBeforeinsertAfterinsertFirstappend,分别对应jQuery的beforeafterprependappend。不过,jQuery还把这几个方法巧妙地调换了调用者与传入参数,衍生出insertBeforeinsertAfterprependToappendTo这几个方法。但不管怎么说,jQuery这样一刀切的做法实现令人不敢苛同。下面是在火狐中实现insertAdjactentXXX家族的一个版本:

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

文章转载自:博客园

为你推荐

  • 推荐视频
  • 推荐活动
  • 推荐产品
  • 推荐文章
  • 慧都慧问
扫码咨询


添加微信 立即咨询

电话咨询

客服热线
023-68661681

TOP