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开发了四个分支方法insertBefore、insertAfter、insertFirst、append,分别对应jQuery的before、after、prepend与append。不过,jQuery还把这几个方法巧妙地调换了调用者与传入参数,衍生出insertBefore、
( 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 ); }; }); |
大致的代码都给出,大家可以各取所需。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步