您好,欢迎访问一九零五行业门户网

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

最近发现各大类库都能利用div.innerhtml=html片断来生成节点元素,再把它们插入到目标元素的各个位置上。这东西实际上就是insertadjacenthtml,但是ie可恶的innerhtml把这优势变成劣势。首先innerhtml会把里面的某些位置的空白去掉,见下面运行框的结果:
复制代码 代码如下:
ie的innerhtml by 司徒正美
另一个可恶的地方是,在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 +
// 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;
},
真是复杂的让人掉眼泪!不过jquery的实现并不太高明,它把插入的东西统统用clean转换为节点集合,再把它们放到一个文档碎片中,然后用appendchild与insertbefore插入它们。在除了火狐外,其他浏览器都支持insertadjactentxxx家族的今日,应该好好利用这些原生api。下面是ext利用insertadjactenthtml等方法实现的domhelper方法,官网给出的数据:
这数据有点老了,而且最新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  = ''; break;
            case link: tw.pre  = 'fixbug';  break;
            default : tw.pre  =   ;
        }
        tw.post = + tw.reverse().join(>) + >;
    }
    var remultitag = /,li
    match = htmlstr.match(remultitag),
    tag = match ? match[1].tolowercase() : ;//解析为    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('')转换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);
    };
});
大致的代码都给出,大家可以各取所需。
其它类似信息

推荐信息