【问题标题】:Why is "element.innerHTML+=" bad code?为什么“element.innerHTML+=”代码不好?
【发布时间】:2012-07-15 22:54:21
【问题描述】:

有人告诉我不要像这样使用element.innerHTML += ... 附加内容:

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

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

它有什么问题?,我还有什么其他选择?

【问题讨论】:

  • 他们有没有告诉你用什么代替element.innerHTML +=?
  • 我怀疑在 2012 年警告过你的人是否有充分的理由这样做。浏览器速度很快,对这个属性的支持几乎是普遍的。除了坚持使用特定库的设施(如果您使用 jQuery,请使用 $() 构造函数创建元素)之外,我再也看不到任何问题了。与往常一样,测试并找出答案。
  • 你应该阅读这篇文章:taligarsiel.com/Projects/howbrowserswork1.htm
  • @Triptych:除了性能还有其他考虑。例如,由于主元素的 DOM 子树完全重建,现有元素上的事件处理程序将被破坏。此外,元素中的任何&lt;script&gt; 都将在某些浏览器中重新运行。最后,不能保证elm.innerHTML = elm.innerHTML 将始终复制元素 DOM 的相同副本。
  • 您没有任何东西可以显示最初的 innerHTML 引用,使用 += 您只会将内容附加到现有内容上。此外,最好使用函数 appendChild 向 DOM 添加内容。

标签: javascript html innerhtml anti-patterns


【解决方案1】:

是的,elm.innerHTML += str; 是一个非常糟糕的主意。
使用 elm.insertAdjacentHTML( 'beforeend', str ) 作为完美的选择。

典型的“浏览器必须重建 DOM”的答案确实不能解决这个问题:

  1. 首先浏览器需要遍历 elm 下的每个元素,它们的每个属性,以及它们的所有文本 & cmets & process 节点,并将它们转义以构建一个字符串。

  2. 然后你有一个长字符串,你追加到它。这一步没问题。

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

  4. 然后它解析字符串,从它刚刚销毁的所有元素、属性和节点构建,以创建一个几乎相同的新 DOM 片段。

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

  6. 但是还没有完成!浏览器还必须通过扫描 all javascript 变量来回收旧节点。

问题:

  • 某些属性可能不会被 HTML 反映,例如 &lt;input&gt; 的当前值将丢失并重置为 HTML 中的初始值。

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

  • 如果您的 js 代码引用了任何旧节点,它们不会被销毁,而是会被孤立。 它们属于文档,但不再位于 DOM 树中。 当您的代码访问它们时,可能不会发生任何事情或可能会引发错误。

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

  • 如果您养成使用 innerHTML 进行 DOM 操作的习惯,您可能会不小心更改属性或做其他您不想做的事情。

  • 您拥有的节点越多,效率越低,电池电量越多。

简而言之,它效率低下,容易出错,简直是懒惰和无知。


最好的选择是Element.insertAdjacentHTML,我没有看到其他答案提到:

elm.insertAdjacentHTML( 'beforeend', str )

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

它允许您将html字符串注入一行元素,包括属性,甚至允许您注入复合元素和多个元素。 它的速度是optimised - 在 Mozilla 的测试中,它的速度要快 150 倍。

如果有人告诉你它不是跨浏览器,它非常有用,它是 HTML5 standard 并在 all browsers 上可用。

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

【讨论】:

  • +1 用于强调 .innerHTML 的问题。我不太相信Element.insertAdjacentHTML 作为替代方案。 .innerHTML 有助于替换元素内的所有内容,但 Element.insertAdjacentHTML 是关于插入,而不是替换。
  • @RahulDesai 这个问题是专门关于插入的,真的。对于完全替换,有时我 removeChild & appendChild / insertAdjacement,有时用 createElement / createContextualFragment 替换Node。
  • 在向 Mozilla 商店发布扩展时,我收到警告:Unsafe call to insertAdjacentHTML Warning: Due to both security and performance concerns, this may not be set using dynamic values which have not been adequately sanitized. This can lead to security issues or fairly serious performance degradation.
  • @VitalyZdanevich 如果我在 google 中找到的 similiar issue 有任何迹象,则警告应该出现在您的输入中,即使您设置 innerHTML 而不是调用 @987654338,您也会收到相同的消息@.
  • 现在 innerHTML 和 insertAdjacentHTML 都被认为是一个坏主意。建议使用 DOMParser 对象。
【解决方案2】:

一种方法(但未经性能测试):(受 DDRRSS 响应启发)

    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));

【讨论】:

    【解决方案3】:

    "element.innerHTML+=" 是错误代码的另一个原因是,最近发现直接更改 innerHTML 属性原则上是不安全的,现在已经反映了这一点,例如在Mozilla's warning about it。

    这是一个不使用innerHTML 属性或insertAdjacentHTML 方法的Javascript 安全/XSS 验证安全/证明实际示例。一些 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 对于需要插入 script nodes which might end up appearing in DOM but not executed 的情况可能太安全了。在这种情况下,可能需要使用这种方法:

    const fragment = document.createRange().createContextualFragment('[some HTML code]');

    【讨论】:

      【解决方案4】:

      短

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

      使用innerHTML 的缺点是您丢失了旧的内容事件处理程序 - 但是您可以使用标记参数来省略它,例如&lt;div onclick="yourfunc(event)"&gt; 在小项目中是 acceptable

      长

      我在 Chrome、Firefox 和 Safari(2019 年 5 月)上进行了性能测试 HERE(您可以在您的机器上运行它们,但请耐心等待 - 大约需要 5 分钟)

      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}
      &lt;b&gt;This snippet NOT test anythig - only presents code used in tests&lt;/b&gt;

      • 对于所有浏览器,innerHTML += 是最慢的解决方案。
      • chrome appendChild 最快的解决方案 - 它比第二个快速解决方案快约 38%,但非常不方便。令人惊讶的是,在 Firefox 上 appendChild 比 innerHTML = 慢。
      • 我们为insertAdjacentHTML str 和innerHTML = str 获得的第二个快速解决方案和类似的性能
      • 如果我们仔细观察innerHTML = innerHTML +str 的情况并与innerHTML = str 进行比较,innerHTML += 最慢的部分似乎是 READING DOM 内容作为字符串(不是将字符串转换为 DOM)李>
      • 如果您想更改 DOM 树,则生成第一个完整字符串(使用 html)并仅更新/重新生成 DOM ONCE
      • 将appendChild 与innerHTML= 混合实际上比纯innerHTML= 慢

      【讨论】:

        【解决方案5】:

        我刚刚学到了为什么 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>
        

        【讨论】:

          【解决方案6】:

          每次设置 innerHTML 时,都必须解析 HTML,构建 DOM 并将其插入到文档中。这需要时间。

          例如,如果elm.innerHTML 有数千个 div、表格、列表、图像等,那么调用 .innerHTML += ... 将导致解析器重新解析所有这些内容 .这也可能会破坏对已构建的 DOM 元素的引用并导致其他混乱。实际上,您要做的只是在末尾追加一个新元素。

          最好直接拨打appendChild:

          var newElement = document.createElement('div');
          newElement.innerHTML = '<div>Hello World!</div>';
          elm.appendChild(newElement);​​​​​​​​​​​​​​​​
          

          这样elm的现有内容就不会再被解析了。

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

          【讨论】:

          • 在这个例子中,他只设置了一次innerHTML,所以我不确定这是否能回答问题。
          • 但是如果目标已经有一些我们想要保留的重要内容(例如附加到正文)怎么办?,这会删除它
          • 随便var s = elem.innerHTML
          • 好吧,innerHTML 中有 200,000 字节的内容。然后,附加一个&lt;div&gt;Hello&lt;/div&gt;。现在,您重新解析 200,000 字节加上单个 DIV 并重新构建整个 DOM。最好只调用一次appendChild() 并直接插入到现有的 DOM 中。
          • 不要忘记它还会销毁已销毁节点上的任何有状态数据。这包括事件处理程序。
          【解决方案7】:

          替代方案是.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>
          

          【讨论】:

            【解决方案8】:

            Mike 的答案可能更好,但另一个考虑因素是您正在处理字符串。 JavaScript 中的字符串连接可能非常慢,尤其是在一些较旧的浏览器中。如果您只是连接来自 HTML 的小片段,那么它可能并不明显,但如果您在页面的主要部分重复附加某些内容,您很可能会在浏览器中看到明显的暂停。

            【讨论】:

              【解决方案9】:

              如果用户使用的是旧版本的 IE(或者也可能是新版本,还没有尝试过),td 上的 innerHTML 会导致问题。 IE中的表格元素是只读的,tsk tsk tsk。

              【讨论】:

                猜你喜欢
                • 2011-04-10
                • 1970-01-01
                • 1970-01-01
                • 2011-03-06
                • 1970-01-01
                • 1970-01-01
                • 2013-05-17
                相关资源
                最近更新 更多