Heim >Web-Frontend >js-Tutorial >深入理解javascript动态插入技术_javascript技巧

深入理解javascript动态插入技术_javascript技巧

WBOY
WBOYOriginal
2016-05-16 17:15:591891Durchsuche

最近发现各大类库都能利用div.innerHTML=HTML片断来生成节点元素,再把它们插入到目标元素的各个位置上。这东西实际上就是insertAdjacentHTML,但是IE可恶的innerHTML把这优势变成劣势。首先innerHTML会把里面的某些位置的空白去掉,见下面运行框的结果:

复制代码 代码如下:




   
       
        <br>            IE的innerHTML By 司徒正美<br>       
       
   

   
       


       


   

另一个可恶的地方是,在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         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
  //如果文档对象里面只有一个标签,如

  //我们大概可能是在外面这样调用它$(this).append("
")
  //这时就直接把它里面的元素名取出来,用document.createElement("div")创建后放进数组返回
  if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) {
    var match = /^$/.exec(elems[0]);
    if ( match )
      return [ context.createElement( match[1] ) ];
  }
  //利用一个div的innerHTML创建众节点
  var ret = [], scripts = [], div = context.createElement("div");
  //如果我们是在外面这样添加$(this).append("表格1","表格1","表格1")
  //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(/(]*?)\/>/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("        [ 1, "" ] ||

        !tags.indexOf("        [ 1, "
", "
" ] ||

        tags.match(/^        [ 1, "", "
" ] ||

        !tags.indexOf("        [ 2, "", "
" ] ||

        // matched above
      (!tags.indexOf("        [ 3, "", "
" ] ||

        !tags.indexOf("        [ 2, "", "
" ] ||

        // IE can't serialize and <script> tags normally <BR> !jQuery.support.htmlSerialize &&//用于创建link元素 <BR> [ 1, "div<div>", "" ] || <br><br> [ 0, "", "" ]; <br><br> // Go to html and back, then peel off extra wrappers <BR> div.innerHTML = wrap[1] + elem + wrap[2];//比如"<table><tbody><tr>" +<td>表格1</script> +""

      // Move to the right depth
      while ( wrap[0]-- )
        div = div.lastChild;

      //处理IE自动插入tbody,如我们使用$('')创建HTML片断,它应该返回
      //'',而IE会返回' '
      if ( !jQuery.support.tbody ) {

        // String was a , *may* have spurious
        var hasBody = /        tbody = !tags.indexOf("
          div.firstChild && div.firstChild.childNodes :

          // String was a bare or
        wrap[1] == "
" && !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;
},

深入理解javascript动态插入技术_javascript技巧

真是复杂的让人掉眼泪!不过jQuery的实现并不太高明,它把插入的东西统统用clean转换为节点集合,再把它们放到一个文档碎片中,然后用appendChild与insertBefore插入它们。在除了火狐外,其他浏览器都支持insertAdjactentXXX家族的今日,应该好好利用这些原生API。下面是Ext利用insertAdjactentHTML等方法实现的DomHelper方法,官网给出的数据:

深入理解javascript动态插入技术_javascript技巧

这数据有点老了,而且最新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 =  /^$/;//匹配单个标签,如

  •     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  = '
  • ')转换HTML片断,它应该返回
        //'',而IE会返回''
        //亦即,在标准浏览器中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                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             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);
        };
    });

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

    Stellungnahme:
    Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn