zoukankan      html  css  js  c++  java
  • javascript 动态插入技术

    最近发现各大类库都能利用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的工作流程吧。

            append: function() {
              //传入arguments对象,true为要对表格进行特殊处理,回调函数
              return this.domManip(arguments, true, function(elem){
                if (this.nodeType == 1)
                  this.appendChild( elem );
              });
            },
            domManip: function( args, table, callback ) {
              if ( this[0] ) {//如果存在元素节点
                var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(),
                //注意这里是传入三个参数
                scripts = jQuery.clean( args, (this[0].ownerDocument || this[0]), fragment ),
                first = fragment.firstChild;
    
                if ( first )
                  for ( var i = 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 );
              }
    
              return this;
    
              function root( elem, cur ) {
                return table && 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.createElement fails in IE with an error but returns typeof 'object'
              if ( typeof context.createElement === "undefined" )
              //确保context为文档对象
                context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
    
              // If a single string is passed in and it's a single tag
              // just do a createElement and skip the rest
              //如果文档对象里面只有一个标签,如<div>
              //我们大概可能是在外面这样调用它$(this).append("<div>")
              //这时就直接把它里面的元素名取出来,用document.createElement("div")创建后放进数组返回
              if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) {
                var match = /^<(\w+)\s*\/?>$/.exec(elems[0]);
                if ( match )
                  return [ context.createElement( match[1] ) ];
              }
              //利用一个div的innerHTML创建众节点
              var ret = [], 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 ( typeof elem === "number" )
                  elem += '';
    
                if ( !elem )
                  return;
    
                // Convert html string into DOM nodes
                if ( typeof elem === "string" ) {
                  // Fix "XHTML"-style tags in all browsers
                  elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){
                    return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ?
                      all :
                      front + "></" + tag + ">";
                  });
    
                  // Trim whitespace, otherwise indexOf won't work as expected
                  var tags = elem.replace(/^\s+/, "").substring(0, 10).toLowerCase();
    
                  var wrap =
                    // option or optgroup
                    !tags.indexOf("<opt") &&
                    [ 1, "<select multiple='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> matched above
                  (!tags.indexOf("<td") || !tags.indexOf("<th")) &&
                    [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ] ||
    
                    !tags.indexOf("<col") &&
                    [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ] ||
    
                    // IE can't serialize <link> and <script> tags normally
                    !jQuery.support.htmlSerialize &&//用于创建link元素
                  [ 1, "div<div>", "</div>" ] ||
    
                    [ 0, "", "" ];
    
                  // Go to html and back, then peel off extra wrappers
                  div.innerHTML = wrap[1] + elem + wrap[2];//比如"<table><tbody><tr>" +<td>表格1</td>+"</tr></tbody></table>"
    
                  // Move to the right depth
                  while ( wrap[0]-- )
                    div = div.lastChild;
    
                  //处理IE自动插入tbody,如我们使用$('<thead></thead>')创建HTML片断,它应该返回
                  //'<thead></thead>',而IE会返回'<thead></thead><tbody></tbody>'
                  if ( !jQuery.support.tbody ) {
    
                    // String was a <table>, *may* have spurious <tbody>
                    var hasBody = /<tbody/i.test(elem),
                    tbody = !tags.indexOf("<table") && !hasBody ?
                      div.firstChild && div.firstChild.childNodes :
    
                      // String was a bare <thead> or <tfoot>
                    wrap[1] == "<table>" && !hasBody ?
                      div.childNodes :
                      [];
    
                    for ( var j = tbody.length - 1; j >= 0 ; --j )
                    //如果是自动插入的里面肯定没有内容
                      if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length )
                        tbody[ j ].parentNode.removeChild( tbody[ j ] );
    
                  }
    
                  // IE completely kills leading whitespace when innerHTML is used
                  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 ( var i = 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] );
                  }
                }
    
                return scripts;//由于动态插入是传入三个参数,因此这里就返回了
              }
      
              return ret;
            },
    

    真是复杂的让人掉眼泪!不过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家族的一个版本:

     
    (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);
        };
    });
    

    大致的代码都给出,大家可以各取所需。

  • 相关阅读:
    序列化二叉树
    把二叉树打印成多行
    按之字形打印数据
    对称的二叉树
    二叉树的下一个结点
    删除链表中重复的结点
    c语言中数组名a和&a详细介绍
    C语言输出格式
    回文素数
    求平均成绩
  • 原文地址:https://www.cnblogs.com/rubylouvre/p/1622631.html
Copyright © 2011-2022 走看看