添加元素
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);