innerHTML误区
添加元素
innerHTML属性是个经常用到的原生属性.将html元素用字符串形式设置到父元素中,经常用于将动态拼接的html文本,显示在父容器里.
拼html字符串的办法已经被认为不妥,正经的办法是使用appendChild()或者createDocumentFragment().不过对于少量的html,当然也能用.
以前用jQuery库,对应的方法是$('#id').html(),在使用原生属性时,发现与jquery行为有所不同.
这可能是原生innerHTML属性在执行时,有判断过程.会判断父元素是否可以接受html文本.而jQuery已经处理过这个问题,它的html()方法分析过html字符串.
以下测试在谷歌浏览器80+版本,x64.
将div的子元素换成a标记
<div id = "div1"></div> let div = document.getElementById('div1'); div.innerHTML = '<a>link</a>'; // 结果 <div class="btn" id="div1"><a>link</a></div>
这是预期的结果,div里可以放a.如果换成tr就不能成功.
div.innerHTML = '<tr><td>data</td></tr>'; // 结果 没有加进去 <div id = "div1"></div>
tr是表格table里的子元素,如果div换成table呢
<table id="tab1"></table> let tab = document.getElementById('tab1'); tab.innerHTML = '<tr><td>data</td></tr>'; // 结果 <table id="tab1"><tbody><tr><td>data</td></tr></tbody></table>
成功了.不过不太一样,table里多了一个tbody元素,而innerHTML时并没有这个tbody.这是原生innerHTML属性自己的行为.
可见,innerHTML也会分析设置的html字符串,如果发现"不合法"的,会拒绝加入.
使用html对象而不是字符串,可以实现预期效果
let tr = document.createElement('tr'); tr.innerHTML = '<td>data</td>'; tab.appendChild(tr); // 结果 <table id="tab1"><tr><td>data</td></tr></table>
建立tr元素对象,使用appendChild(tr)加到table,结果里没有tboby元素.
在项目中还是减少使用innerHTML,使用正规的appendChild()和createDocumentFragment()
执行js
当innerHTML一段html里包含js时,是不会执行的.最常见的情况是,从服务器ajax一段包含html,js的页面片段,使用innerHTML设置到容器div,结果js不执行.
如果使用jQuery的$('#id').html() 方法,不会有这个问题.js执行了.显然,jQuery的html()方法是做了工作的.查看源码时,发现jQuery解析了html字符串.
innerHTML里的js不执行,可能和浏览器的机制有关.innerHTML里的js可能被当成一般的文本了,所以不执行.
对于一段html,js混合的文本,尝试过以下方法可以让js执行.
一. 使用createContextualFragment()方法
let htmlString='<div>let range = document.createRange();</div><script src="abc.js"><\/script><script>console.log("hello world")<\/script>'; // 低版本浏览器不支持这个方法 let range = document.createRange(); let fragment = range.createContextualFragment(htmlString); document.body.appendChild(fragment);
这个方法解析htmlString,然后返回DocumentFragment对象,加到文档后,js会执行.
但是有一个问题,执行时没有按script标记的顺序.先执行了第二个script,后执行的abc.js
二. 解析法
使用js生成新的script标记,再添加到文档,是可以执行的.这个办法是分析htmlString,将script找出来,再重新生成一次.
为了让js顺序执行,可以在解析时将外联的js下载,变成内联的.具体做法,递归解析htmlString.
这段代码解析html字符串,返回一个文档片段对象,里面的js标记是重新生成的,对于外联会下载成内联,顺序执行.
代码有局限.html字符串中的script标记只能是第一子节点,不能包含在其它dom元素内,因为递归方法只遍历了子节点,没有遍历后代节点.
只能满足相对简单的script标记顺序执行,对于有复杂依赖的js,也不能保证顺序执行.
// 解析html, val:html字符串 ,onReady:解析完成后的文档片段对象 function parseHtml = (val, onReady) => { let framgSource; if (typeof val === 'string') { let range = document.createRange(); framgSource = range.createContextualFragment(val); } else if (val instanceof DocumentFragment) { framgSource = val; } else if (val.length) { framgSource = document.createDocumentFragment(); framgSource.append(...val); } else { framgSource = document.createDocumentFragment(); framgSource.append(val); } // 放入fragment.(解析放入) let fragment = document.createDocumentFragment(); _parseHtmlNodeLoad(fragment, framgSource, onReady); }; // 递归 function _parseHtmlNodeLoad = (toFragm, fromFragm, onReady) => { if (fromFragm.firstChild === null) { onReady(toFragm); return; } // script元素.设置到innerhtml时不会执行,要新建一个script对象,再添加 if (fromFragm.firstChild.nodeName === 'SCRIPT') { let newScript = document.createElement('script'); let src = fromFragm.firstChild.src; if (src) { // 外联的script,要加载下来,否则有执行顺序问题.外联的没有加载完,内联的就执行了.如果内联js依赖外联则出错. // 这个办法是获取js脚本,是设置到生成的script标签中.(变成内联的了) fetch(src).then(res => res.text()) .then((js) => { newScript.innerHTML = js; toFragm.append(newScript); fromFragm.removeChild(fromFragm.firstChild); _parseHtmlNodeLoad(toFragm, fromFragm, onReady); }); } else { // 内联的直接设置innerHtml newScript.innerHTML = fromFragm.firstChild.innerHTML; toFragm.append(newScript); fromFragm.removeChild(fromFragm.firstChild); _parseHtmlNodeLoad(toFragm, fromFragm, onReady); } } else { // 其它元素 toFragm.append(fromFragm.firstChild); _parseHtmlNodeLoad(toFragm, fromFragm, onReady); } };
缓存文档片段
有时需要将文档中的一部分dom缓存起来,需要时再加入文档中.
由于innerHTML是字符串,所以一些dom的属性不会保存.比如select元素,在缓存前选择了"two",使用innerHTML缓存在还原时,选择会变成默认的"one".
使用node.cloneNode(true)复制节点方法也不行,也保存不了select元素的选中状态.
可以使用DocumentFragment对象的append()方法,添加这个div后,div会脱离文档,缓存到文档片段对象中.在放入文档中,它的状态不变.
这个办法没有"加工"要缓存的元素,只是将它移动了位置.从文档对象移动到文档片段对象.
// select 选择了two <select> <option value="1">one</option> <option value="2">two</option> <option value="3">three</option> </select> // 使用innerHTML,将div的所有子元素存到变量中 let dom = div.innerHTML; div.innerHTML = ''; // 还原 (选择状态会丢失) div.innerHTML = dom; // cloneNode(true) (复制select节点,选择状态也会丢失) dom = div.cloneNode(true); div.append(dom); // 使用DocumentFragment对象的append()添加到文档片段对象,再放入文档中,状态不变. dom = document.createDocumentFragment(); dom.append(div); div.append(dom);