In depth understanding of javascript dynamic insertion techniques

  • 2020-03-29 23:42:15
  • OfStack

It has recently been discovered that major libraries can use div.innerhtml =HTML fragments to generate node elements and insert them into various locations of the target element. This thing is actually insertAdjacentHTML, but damned innerHTML IE the advantage into a disadvantage. First, innerHTML removes some of the white space from the inside of the box.



<!doctype html>
<html dir="ltr" lang="zh-CN">
    <head>
        <meta charset="utf-8" />
        <title>
            IE the innerHTML By  SiTuZhengMei 
        </title>
        <script type="text/javascript">
            window.onload = function() {
                var div = document.createElement("div");
                div.innerHTML = "   <td>    <b> Stuart </b> Is beauty          </td>        "
                alert("|" + div.innerHTML + "|");
                var c = div.childNodes;
                alert(" Number of generated nodes   " + c.length);
                for(var i=0,n=c.length;i<n;i++){
                      alert(c[i].nodeType);
                      if(c[i].nodeType === 1){
                          alert(":: "+c[i].childNodes.length);
                      }
                }        
            }
        </script>
    </head>

    <body>
        <p id="p">
        </p>
    </body>
</html>

Another bad thing is that in IE the innerHTML of the following elements is read-only: col, colgroup, frameset, HTML, head, style, table, tbody, tfoot, thead, title, and tr. To get rid of them, Ext had an insertIntoTable. InsertIntoTable is added using DOM insertBefore and appendChild, which is basically the same as jQuery. However, jQuery is totally dependent on these two methods, Ext also used insertAdjacentHTML. To be more efficient, document fragmentation is used consistently in all libraries. The basic process is to extract the node from div.innerhtml, then move to the document fragment, then insert the node with insertBefore and appendChild. For firefox, Ext also USES createContextualFragment to parse the text and insert it directly into its target location. Obviously, Ext is much faster than jQuery. However, jQuery inserts not only HTML fragments, but also various nodes and jQuery objects. Let's revisit jQuery's workflow.


append: function() { 
  //Arguments object passed in, true for the special processing of the table, callback function
  return this.domManip(arguments, true, function(elem){ 
    if (this.nodeType == 1) 
      this.appendChild( elem ); 
  }); 
}, 
domManip: function( args, table, callback ) { 
  if ( this[0] ) {//If there is an element node
    var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(), 
    //Notice that we're passing in three parameters
    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 is a arguments object, context is a document object, and fragment is an empty 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" ) 
  //Make sure that the context is a document object
    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 
  //If there is only one label in the document object, such as <Div>
  //We probably called it from the outside like this $(this).append("<Div>" )
  //At this point, simply take the name of the element inside it, create it with document.createelement ("div") and return it to the array
  if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) { 
    var match = /^<(w+)s*/?>$/.exec(elems[0]); 
    if ( match ) 
      return [ context.createElement( match[1] ) ]; 
  } 
  //Create multiple nodes using the innerHTML of a div
  var ret = [], scripts = [], div = context.createElement("div"); 
  //If we were to add $(this).append("<Td> Table 1 </ td>" , "<Td> Table 1 </ td>" , "<Td> Table 1 </ td>" )
  //JQuery. Each traverses aguments, callback. Call (value, I, value) with its fourth branch (no arguments, length)
  jQuery.each(elems, function(i, elem){//I is the index and elem is the element in the arguments object
    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 &&//Used to create the link element
      [ 1, "div<div>", "</div>" ] || 

        [ 0, "", "" ]; 

      // Go to html and back, then peel off extra wrappers 
      div.innerHTML = wrap[1] + elem + wrap[2];//Such as "<Table> <Tbody> <Tr>" + <Td> Table 1 </ td> + "</ tr> </ tbody> </ table>"

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

      //Handle IE to insert tbody automatically, if we use $('<Thead> </ thead> ') create the HTML fragment, which should return
      //'<Thead> </ thead> 'and IE will return '<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 it's automatically inserted, there's no content in it
          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 ); 
     //Make all the nodes into a pure array
      elem = jQuery.makeArray( div.childNodes ); 
    } 

    if ( elem.nodeType ) 
      ret.push( elem ); 
    else
    //Merge the two arrays, and the merge method deals with the param elements that are missing from the object element under IE
      ret = jQuery.merge( ret, elem ); 

  }); 

  if ( fragment ) { 
    for ( var i = 0; ret[i]; i++ ) { 
      //If the first layer of childNodes has script element nodes, collect them in scripts for dynamic execution with 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 { 
        //Traverse the nodes of each layer to collect script element nodes
        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;//Since dynamic insertion is passed in three parameters, it is returned here
  } 

  return ret; 
}, 

< img border = 0 SRC = "/ / files.jb51.net/file_images/article/201311/20131112145628668.jpg" >

< img border = 0 SRC = "/ / files.jb51.net/file_images/article/201311/20131112145751246.jpg" >

This data is a bit old, and the latest 3.03 already solved in IE table insert content for (such as table, tbody tr innerHTML is read-only, insertAdjactentHTML, pasteHTML method can modify the content, use standard DOM methods and slow, Ext early version of here). As you can see, after combining insertAdjactentHTML and document fragments, IE6 insert node have been incredible speed improvement, matches the firefox. Based on this, Ext has developed four branch methods insertBefore, insertAfter, insertFirst, and append, which correspond to jQuery's before, after, prepend, and append, respectively. However, jQuery also cleverly swaps the caller with the incoming arguments to spawn off insertBefore, insertAfter, prependTo, and appendTo. But in any case, jQuery's one-size-fits-all approach to the implementation is not to be taken seriously. The following is implemented in firefox insertAdjactentXXX family a version:


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

We can use it to design a faster and more reasonable dynamic insertion method. Here are some of my implementations:


//Four insertion method, corresponding to the four insert position of insertAdjactentHTML name to use jQuery
//Stuff can be a string, a variety of nodes, or a dom object (a class array object, easy to chain!).
//The code is more concise and beautiful than the jQuery implementation.
    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"); 
        }); 
    } 

Both of them call two static methods,batch and insert. Since dom objects are array like objects, I implemented several important iterators for them, like jQuery, forEach, map, filter, and so on. A dom object contains a complex number of dom elements, so we can use forEach to iterate over them, executing the callback method.


batch:function(els,callback){ 
    els.forEach(callback); 
    return els;//The chain operation
},

The insert method performs the function of jQuery's domManip method (dojo is the place method), but the insert method handles one element node at a time, unlike jQuery, which handles a set of element nodes. The cluster processing has been decoupled from the batch method above.


insert : function(el,stuff,where){ 
     //Defines two global things that provide internal method calls
     var doc = el.ownerDocument || dom.doc, 
     fragment = doc.createDocumentFragment(); 
     if(stuff.version){//If it is a dom object, the element nodes inside it are moved to the document fragment
         stuff.forEach(function(el){ 
             fragment.appendChild(el); 
         }) 
         stuff = fragment; 
     } 
     //For firefox and Internet explorer part of the element call
     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; 
         } 
     }; 
      //For firefox to call
     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); 
     }; 
     //The following elements of the innerHTML in IE are read-only, call insertAdjacentElement insertion will go wrong
     //Col, colgroup, frameset, HTML, head, style, title,table, tbody, tfoot, thead, and tr;
     dom._insertAdjacentIEFix = function(el,htmlStr,where){ 
         var parsedHTML = dom.parseHTML(htmlStr,fragment); 
         dom._insertAdjacentElement(el,parsedHTML,where) 
     }; 
     //If it's a node, make a copy
     stuff = stuff.nodeType ?  stuff.cloneNode(true) : stuff; 
     if (el.insertAdjacentHTML) {//Ie, chrome, opera, safari have implemented insertAdjactentXXX family
         try{//Suitable for opera,safari,chrome and IE
             el['insertAdjacent'+ (stuff.nodeType ? 'Element':'HTML')](where,stuff); 
         }catch(e){ 
             //IE certain elements called insertAdjacentXXX might be wrong, so use this patch
             dom._insertAdjacentIEFix(el,stuff,where); 
         }      
     }else{ 
         //Firefox is special
         dom['_insertAdjacent'+ (stuff.nodeType ? 'Element':'HTML')](el,stuff,where); 
     } 
 } 


parseHTML : function(htmlStr, fragment){ 
    var div = dom.doc.createElement("div"), 
    reSingleTag =  /^<(w+)s*/?>$/;//Matches a single label, such as <Li>
    htmlStr += ''; 
    if(reSingleTag.test(htmlStr)){//If STR is a single label
        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:]+)/,//Matches a pair of tags or multiple tags, such as <Li> </ li> , li
    match = htmlStr.match(reMultiTag), 
    tag = match ? match[1].toLowerCase() : "";//Resolve to <Li, li
    if(match && tagWrap[tag]){ 
        var wrap = tagWrap[tag]; 
        div.innerHTML = wrap.pre + htmlStr + wrap.post; 
        n = wrap.length; 
        while(--n >= 0)//Return what we have added
            div = div.lastChild; 
    }else{ 
        div.innerHTML = htmlStr; 
    } 
    //Handle IE to insert tbody automatically, if we use dom. ParseHTML ('<Thead> </ thead> ') convert the HTML fragment, which should return
    //'<Thead> </ thead> 'and IE will return '<Thead> </ thead> <Tbody> </ tbody> '
    //That is, in a standard browser, return div.children.length returns 1 and IE returns 2
    if(dom.feature.autoInsertTbody && !!tagWrap[tag]){ 
        var ownInsert = tagWrap[tag].join('').indexOf("tbody") !== -1,//We inserted
        tbody = div.getElementsByTagName("tbody"), 
        autoInsert = tbody.length > 0;//Insert the IE
        if(!ownInsert && autoInsert){ 
            for(var i=0,n=tbody.length;i<n;i++){ 
                if(!tbody[i].childNodes.length )//If it's automatically inserted, there's no content in it
                    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)){ //Move the node on the div to the document fragment!
            fragment.appendChild(firstChild); 
        } 
        return fragment; 
    } 
    return div.children; 
} 

Well, that's basically it, it runs a lot faster than jQuery, and the code is nice, at least not as messy as jQuery. JQuery also has four inversion methods. Here is the jQuery implementation:


jQuery.each({ 
    appendTo: "append", 
    prependTo: "prepend", 
    insertBefore: "before", 
    insertAfter: "after", 
    replaceAll: "replaceWith"
}, function(name, original){ 
    jQuery.fn[ name ] = function( selector ) {//Inserts (HTML, element nodes, jQuery objects)
        var ret = [], insert = jQuery( selector );//Converts the insert into a jQuery object
        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 );//Calls four implemented insert methods
            ret = ret.concat( elems ); 
        } 
        return this.pushStack( ret, name, selector );//Since the chain operation is not separated from the code, it needs to be implemented by itself
    }; 
}); 

My realization:


dom.each({ 
    appendTo: 'append', 
    prependTo: 'prepend', 
    insertBefore: 'before', 
    insertAfter: 'after'
},function(method,name){ 
    dom.prototype[name] = function(stuff){ 
        return dom(stuff)[method](this); 
    }; 
}); 

The general code is given, so you can take what you want.


Related articles: