为什么 "element.innerHTML+=" 是糟糕的代码?

65

有人告诉我不要使用element.innerHTML += ...这样的方式添加内容:

var str = "<div>hello world</div>";
var elm = document.getElementById("targetID");

elm.innerHTML += str; //not a good idea?

这个有什么问题?还有其他的替代方案吗?


4
他们告诉你要使用什么替代 element.innerHTML += 吗? - laurent
1
我怀疑在2012年警告你不要这样做的人没有充分的理由。浏览器速度很快,对于这个属性的支持几乎是普遍的。除了坚持使用特定库的功能(如果你正在使用jQuery,请使用$()构造函数创建元素),我不再看到任何问题。不过,像往常一样,测试并找出答案。 - Kenan Banks
你应该阅读这篇文章:http://taligarsiel.com/Projects/howbrowserswork1.htm - Hacker Wins
5
@Triptych:除了性能以外,还有其他需要考虑的因素。例如,由于主元素的DOM子树会被完全重构,已经存在的元素上的事件处理程序将被销毁。此外,在某些浏览器中,元素内的任何<script>标记都将重新运行。最后,不能保证 elm.innerHTML = elm.innerHTML 总是能够产生一个相同的元素DOM结构的副本。 - Tim Down
你没有任何东西显示innerHTML最初的引用,使用+=只会将内容附加到现有内容上。此外,最好使用appendChild函数将内容添加到DOM中。 - SPlatten
9个回答

64
每次设置 innerHTML 都需要解析 HTML,构建 DOM 并将其插入文档中。这需要时间。
例如,如果 elm.innerHTML 有数千个 div、table、列表、图像等,那么调用 .innerHTML += ... 将导致解析器重新解析所有的内容。这可能会破坏对已构造的 DOM 元素的引用并引起其他混乱。实际上,你只想在末尾添加一个新元素。
最好只调用 appendChild
var newElement = document.createElement('div');
newElement.innerHTML = '<div>Hello World!</div>';
elm.appendChild(newElement);​​​​​​​​​​​​​​​​

这样,现有的elm内容不会再次被解析。

注意:某些浏览器可能足够聪明以优化+=运算符,而不重新解析现有的内容。我没有研究过这一点。


4
在这个例子中,尽管他只设置了一次innerHTML,但我不确定这是否回答了问题。 - laurent
但是如果目标已经有一些我们想要保留的重要内容(例如追加到正文中),这将会抹掉它。 - ajax333221
2
好的,假设 innerHTML 中有 200,000 字节的内容。然后,您附加了一个单独的 <div>Hello</div>。现在,您需要重新解析 200,000 字节加上单个 DIV 并重新构建整个 DOM。更好的方法是只调用一次 appendChild() 并直接插入到现有的 DOM 中。 - Mike Christensen
1
不要忘记,它还会摧毁在被销毁的节点上的任何有状态数据,包括事件处理程序。 - user1106925
@amnotiam - 是的,这就是我所指的“引起其他混乱”的意思。 - Mike Christensen
显示剩余5条评论

37

是的,elm.innerHTML += str; 是一个非常糟糕的想法。
请使用 elm.insertAdjacentHTML('beforeend', str) 作为完美的替代方案。

典型的“浏览器必须重建 DOM”答案并不恰当:

  1. 首先,浏览器需要遍历 elm 下的每个元素、它们的各种属性以及所有文本、注释和处理节点,并对它们进行转义以构建字符串。

  2. 然后你得到了一个长字符串,将其追加到现有的字符串之后。这一步没有问题。

  3. 第三步,当您设置 innerHTML 时,浏览器必须删除刚才经过的所有元素、属性和节点。

  4. 然后它解析由所有被摧毁的元素、属性和节点组成的字符串,以创建一个基本相同的新 DOM 片段。

  5. 最后,它会附加新节点,浏览器必须重新布局整个内容。这可能是可避免的(见下面的替代方案),但即使追加的节点需要进行布局,旧节点的布局属性也会被缓存,而不是从头重新计算。

  6. 但还没完呢!浏览器还必须通过扫描 所有 JavaScript 变量来回收旧节点。

问题:

  • 某些属性可能不会反映在 HTML 中,例如 <input> 的当前值将丢失并重置为 HTML 中的初始值。

  • 如果您在旧节点上有任何事件处理程序,它们将被销毁,您必须重新附加所有事件处理程序。

  • 如果您的 JS 代码引用了旧节点,则它们不会被销毁,而是变成孤儿节点。 它们仍然属于文档,但不再在 DOM 树中。 当您的代码访问它们时,可能什么也不会发生,或者可能会抛出错误。

  • 这两个问题都意味着它与 js 插件不兼容 - 插件可能会附加处理程序或保留对旧节点的引用并导致内存泄漏。

  • 如果您习惯使用 innerHTML 进行 DOM 操作,可能会意外更改属性或执行其他意想不到的操作。

  • 节点越多,这种方法就越低效,而且电池很容易耗尽。

简而言之,这是低效、容易出错的,它只是一种懒惰和无知的行为。


最好的替代方案是Element.insertAdjacentHTML,我在其他答案中没有看到:

elm.insertAdjacentHTML( 'beforeend', str )

几乎相同的代码,没有innerHTML的问题。 无需重建,无处理程序丢失,无输入重置,更少的内存碎片化,没有坏习惯,也没有手动创建和指定元素。

它允许你在一行中将HTML字符串注入到元素中,包括属性,甚至允许您注入复合和多个元素。 它的速度经过优化 - 在Mozilla的测试中快了150倍。

如果有人告诉你它不是跨浏览器的,请注意它如此有用,以至于它是HTML5的标准并可在所有浏览器上使用。

永远不要再写elm.innerHTML+=了。


2
+1 强调了 .innerHTML 的问题。虽然 Element.insertAdjacentHTML 是一种替代方案,但我并不太信服。.innerHTML 有助于替换元素内的所有内容,而 Element.insertAdjacentHTML 则是关于插入而非替换。 - Rahul Desai
1
@RahulDesai 这个问题特别涉及到插入操作。对于完全替换,有时我会使用removeChild和appendChild / insertAdjacement,有时则使用replaceNode与createElement / createContextualFragment。 - Sheepy
在将扩展程序发布到Mozilla商店时,我收到了警告:“不安全的调用insertAdjacentHTML。警告:由于安全和性能方面的考虑,不能使用未经充分净化的动态值进行设置。这可能会导致安全问题或相当严重的性能下降。” - Vitaly Zdanevich
现在,innerHTML和insertAdjacentHTML都被认为是不好的选择。建议使用DOMParser对象。 - DDRRSS
它确实更重,我一点也不喜欢,但由于XSS安全问题的验证警告,Mozilla最近强制我使用它以避免这种情况。 https://discourse.mozilla.org/t/question-concerning-practical-alternatives-to-innerhtml-and-insertadjacent-html-passing-amo-review/34806/15 - DDRRSS
显示剩余6条评论

9

简短

如果你将innerHTML += ...(更新内容)更改为innerHTML = ...(重新生成内容),那么你将得到非常快的代码。看起来+=最慢的部分是将DOM内容作为字符串读取(而不是将字符串转换为DOM)。

使用innerHTML的缺点是你会失去旧内容的事件处理程序 - 但是你可以使用标记参数来省略这个问题,例如<div onclick="yourfunc(event)">,在小项目中可以接受

详细

我在Chrome、Firefox和Safari上进行了性能测试(此处链接)(2019年5月)(你可以在你的机器上运行它们,但需要耐心等待约5分钟)。

enter image description here

function up() {
  var container = document.createElement('div');
  container.id = 'container';
  container.innerHTML = "<p>Init <span>!!!</span></p>"
  document.body.appendChild(container);
}

function down() {
  container.remove()
}

up();

// innerHTML+=
container.innerHTML += "<p>Just first <span>text</span> here</p>";
container.innerHTML += "<p>Just second <span>text</span> here</p>";
container.innerHTML += "<p>Just third <span>text</span> here</p>";

down();up();

// innerHTML += str
var s='';
s += "<p>Just first <span>text</span> here</p>";
s += "<p>Just second <span>text</span> here</p>";
s += "<p>Just third <span>text</span> here</p>";
container.innerHTML += s;

down();up();

// innerHTML = innerHTML+str
var s=container.innerHTML+'';
s += "<p>Just first <span>text</span> here</p>";
s += "<p>Just second <span>text</span> here</p>";
s += "<p>Just third <span>text</span> here</p>";
container.innerHTML = s;

down();up();

// innerHTML = str
var s="<p>Init <span>!!!</span></p>";
s += "<p>Just first <span>text</span> here</p>";
s += "<p>Just second <span>text</span> here</p>";
s += "<p>Just third <span>text</span> here</p>";
container.innerHTML = s;

down();up();

// insertAdjacentHTML str
var s='';
s += "<p>Just first <span>text</span> here</p>";
s += "<p>Just second <span>text</span> here</p>";
s += "<p>Just third <span>text</span> here</p>";
container.insertAdjacentHTML("beforeend",s);

down();up();

// appendChild
var p1 = document.createElement("p");
var s1 = document.createElement("span"); 
s1.appendChild( document.createTextNode("text ") );
p1.appendChild( document.createTextNode("Just first ") );
p1.appendChild( s1 );
p1.appendChild( document.createTextNode(" here") );
container.appendChild(p1);

var p2 = document.createElement("p");
var s2 = document.createElement("span"); 
s2.appendChild( document.createTextNode("text ") );
p2.appendChild( document.createTextNode("Just second ") );
p2.appendChild( s2 );
p2.appendChild( document.createTextNode(" here") );
container.appendChild(p2);

var p3 = document.createElement("p");
var s3 = document.createElement("span"); 
s3.appendChild( document.createTextNode("text ") );
p3.appendChild( document.createTextNode("Just third ") );
p3.appendChild( s3 );
p3.appendChild( document.createTextNode(" here") );
container.appendChild(p3);

down();up();

// insertAdjacentHTML
container.insertAdjacentHTML("beforeend","<p>Just first <span>text</span> here</p>");
container.insertAdjacentHTML("beforeend","<p>Just second <span>text</span> here</p>");
container.insertAdjacentHTML("beforeend","<p>Just third <span>text</span> here</p>");

down();up();

// appendChild and innerHTML
var p1 = document.createElement('p');
p1.innerHTML = 'Just first <span>text</span> here';
var p2 = document.createElement('p');
p2.innerHTML = 'Just second <span>text</span> here';
var p3 = document.createElement('p');
p3.innerHTML = 'Just third <span>text</span> here';
container.appendChild(p1);
container.appendChild(p2);
container.appendChild(p3);
b {color: red}
<b>This snippet NOT test anythig - only presents code used in tests</b>


  • 对于所有浏览器,innerHTML += 是最慢的解决方案。
  • 在Chrome中,最快的解决方案是使用appendChild,比第二个快速解决方案快约38%,但它非常不方便。令人惊讶的是,在Firefox上,appendChildinnerHTML = 更慢。
  • 第二种快速解决方案和类似的性能我们可以通过 insertAdjacentHTML strinnerHTML = str 来实现。
  • 如果我们更仔细地观察 innerHTML = innerHTML +str 的情况,并将其与 innerHTML = str 进行比较,就会发现 innerHTML += 最慢的部分是将DOM内容作为字符串读取(而不是将字符串转换为DOM)
  • 如果您想更改DOM树,请先生成整个带有HTML的字符串,然后仅在一次更新/重新生成DOM。
  • 混合使用 appendChildinnerHTML= 实际上比纯粹的 innerHTML= 更慢。

8

另一种方法是使用.createElement().textContent.appendChild()。如果你处理大量数据,使用+=添加可能会出现问题。

演示:http://jsfiddle.net/ThinkingStiff/v6WgG/

脚本

var elm = document.getElementById( 'targetID' ),
    div = document.createElement( 'div' );
div.textContent = 'goodbye world';
elm.appendChild( div );

HTML

<div id="targetID">hello world</div>

2

如果用户使用较旧版本的IE(或者可能是更新版本,我没有尝试过),在td上使用innerHTML会导致问题。在IE中,表格元素是只读的,真是太糟糕了。


2

我刚刚吃了亏,才明白为什么innerHTML不好用。在下面的代码中,当你设置innerHTML时,Chrome会丢失onclick事件。jsFiddle

var blah = document.getElementById('blah');
var div = document.createElement('button');
div.style['background-color'] = 'black';
div.style.padding = '20px;';
div.style.innerHTML = 'a';
div.onclick = () => { alert('wtf');};

blah.appendChild(div);

// Uncomment this to make onclick stop working
blah.innerHTML += ' this is the culprit';

<div id="blah">
</div>

1
Mike的回答可能更好,但另一个考虑因素是你正在处理字符串。在JavaScript中,字符串连接可能会非常慢,特别是在一些旧浏览器中。如果你只是从HTML中连接一些小片段,那么可能不会注意到,但如果你有页面的主要部分需要重复添加内容,你很可能会在浏览器中看到明显的暂停。

1

一种方法(但未经过性能测试):(灵感来自DDR RSS的回应)

    const parser = new DOMParser();
    const parsedBody = parser.parseFromString(str, 'text/html').body;
    for(let i = 0; i <= parsedBody.childNodes.length; i++){
        const tag = parsedBody.childNodes[i];
        if(!tag) continue;

        if(tag instanceof Text){
            codeElement.append(document.createTextNode(tag.textContent));
        } else if(tag instanceof HTMLElement){
            codeElement.appendChild(tag.cloneNode(true));
        }}

    codeElement.appendChild(document.createTextNode(parsedBody.innerText));

0

"element.innerHTML+="是糟糕的代码的另一个原因是,直接更改innerHTML属性在原则上被发现是不安全的,这现在反映在Mozilla对此的警告中。

这里有一个JavaScript安全/XSS验证安全/证明实际工作的实例,而不使用innerHTML属性或insertAdjacentHTML方法。某个htmlElement的内容得到更新-替换为新的HTML内容:

const parser = new DOMParser(),
      tags = parser.parseFromString('[some HTML code]'), `text/html`).body.children, 
      range = document.createRange();
range.selectNodeContents(htmlElement);
range.deleteContents();
for (let i = tags.length; i > 0; i--)
{
     htmlElement.appendChild(tags[0]); 
     // latter elements in HTMLCollection get automatically emptied out with each use of
     // appendChild method, moving later elements to the first position (0), so 'tags' 
     // can not be walked through normally via usual 'for of' loop
}

然而,解析器生成的DOM可能对于需要插入可能出现在DOM中但未执行的脚本节点的情况来说过于安全。在这种情况下,可以使用以下方法:

const fragment = document.createRange().createContextualFragment('[一些HTML代码]');


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接