能否在不破坏子元素事件监听器的情况下追加innerHTML?

169
在下面的示例代码中,我将onclick事件处理程序附加到包含文本“foo”的span上。该处理程序是一个匿名函数,弹出一个alert()
但是,如果我给父节点的innerHTML分配值,则此onclick事件处理程序会被销毁 - 单击“foo”无法弹出警报框。
这个问题能修复吗?
<html>
 <head>
 <script type="text/javascript">

  function start () {
    myspan = document.getElementById("myspan");
    myspan.onclick = function() { alert ("hi"); };

    mydiv = document.getElementById("mydiv");
    mydiv.innerHTML += "bar";
  }

 </script>
 </head>

 <body onload="start()">
   <div id="mydiv" style="border: solid red 2px">
     <span id="myspan">foo</span>
   </div>
 </body>

</html>

这个问题与“向表单添加新字段会删除用户输入”的问题有关。所选答案非常好地解决了这两个问题。 - MattT
事件委托可以用来解决这个问题。 - dasfdsa
13个回答

182

使用.insertAdjacentHTML()可以保留事件监听器,并且被主流浏览器所支持。它是.innerHTML的简单一行替代方法。

var html_to_insert = "<p>New paragraph</p>";

// with .innerHTML, destroys event listeners
document.getElementById('mydiv').innerHTML += html_to_insert;

// with .insertAdjacentHTML, preserves event listeners
document.getElementById('mydiv').insertAdjacentHTML('beforeend', html_to_insert);

'beforeend' 参数指定了将 HTML 内容插入到元素的哪个位置。选项包括 'beforebegin''afterbegin''beforeend''afterend',它们对应的位置分别是:

<!-- beforebegin -->
<div id="mydiv">
  <!-- afterbegin -->
  <p>Existing content in #mydiv</p>
  <!-- beforeend -->
</div>
<!-- afterend -->

1
请注意,Chrome / Edge 对大小写敏感:即 afterbegin 不被接受,而 afterBegin 则可以。 - AK_
2
@AK_ 规范 保证字符串匹配不区分大小写。 - Sebastian Simon

158

不幸的是,对 innerHTML 进行赋值会导致所有子元素的破坏,即使您正在尝试追加。如果您想保留子节点(及其事件处理程序),则需要使用 DOM 函数

function start() {
    var myspan = document.getElementById("myspan");
    myspan.onclick = function() { alert ("hi"); };

    var mydiv = document.getElementById("mydiv");
    mydiv.appendChild(document.createTextNode("bar"));
}

编辑: 来自评论的Bob的解决方案。发布你的答案,Bob!为此获得荣誉。 :-)

function start() {
    var myspan = document.getElementById("myspan");
    myspan.onclick = function() { alert ("hi"); };

    var mydiv = document.getElementById("mydiv");
    var newcontent = document.createElement('div');
    newcontent.innerHTML = "bar";

    while (newcontent.firstChild) {
        mydiv.appendChild(newcontent.firstChild);
    }
}

84
新内容 = document.createElement('div'); 新内容.innerHTML = 任意的数据块; while (新内容.firstChild) mydiv.appendChild(新内容.firstChild);(翻译:上面这段代码的作用是创建一个名为“新内容”的div元素,并将任何类型的数据块(比如文字、图像等)插入到该元素中。之后,通过while循环将新内容中的所有子元素逐个添加到mydiv元素中。) - bobince
@Ben:你不需要调用newcontent.removeChild()。Bob的原始评论是正确的。 - Crescent Fresh
2
哦,最后一件事,你会想要使用“var myspan”,“var newcontent”等来避免意外泄露全局变量。 - bobince
呵呵,我越来越意识到,在SO上匆忙编写的示例/原型代码并不对我有好处。 :-) - Ben Blank
@Omar,为什么它没有按预期工作?像 const newContent = Object.assign(document.createElement("div"), { innerHTML: arbitraryBlob }); while(newContent.firstChild){ mydiv.appendChild(newcontent.firstChild); } 这样带有循环的结构是可以正常工作的。newContent.firstChild 可以是一个节点或者 nullappendChild 移动一个节点——节点不能同时存在于两个位置。因此,当 newContent.firstChild === null 时,循环最终停止;此时,newContent 的所有子节点都已移动到目标 mydiv 中——而不会影响任何现有的子节点。 - Sebastian Simon
显示剩余6条评论

3
我创建了一个标记,以字符串形式插入,因为它比使用复杂的DOM更简洁、易读。
然后我将其设置为临时元素的 innerHTML,以便只取该元素的唯一子元素并将其附加到 body。
var html = '<div>';
html += 'Hello div!';
html += '</div>';

var tempElement = document.createElement('div');
tempElement.innerHTML = html;
document.getElementsByTagName('body')[0].appendChild(tempElement.firstChild);

3
现在是2012年,jQuery有append和prepend函数可以实现这一点,添加内容而不影响当前内容。非常有用。

15
.insertAdjacentHTML从IE4时代就存在。 - Esailija
1
@Tynach 是的,它支持。这是他们 JavaScript 网站上最好的文档:https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML - Jordon Bedwell
2
@JordonBedwell,老实说,我不知道为什么之前会说那样的话。你是100%正确的。我感觉当时我简单地查看了一下,但是找不到它,但是...老实说我没有任何借口。Esailija甚至链接到那个页面。 - Tynach
2
OP并不是在谈论jQuery。 - user3303864
1
jQuery梗图乐趣 - Ruan Mendes

1
作为一个轻微(但相关的)旁注,如果您使用JavaScript库(如jQuery v1.3)来进行DOM操作,则可以利用实时事件,其中您设置一个处理程序,例如:
 $("#myspan").live("click", function(){
  alert('hi');
});

并且它将在任何类型的jQuery操作期间始终应用于该选择器。有关实时事件,请参见:docs.jquery.com/events/live 有关jQuery操作,请参见:docs.jquery.com/manipulation


4
OP没有谈论jQuery。 - user3303864

1

如果您直接在模板中使用类似于<body onload="start()">的标签属性onclick="sayHi()"绑定事件,类似于angular/vue/react等框架,那么是可以实现的。您还可以使用<template>来操作“动态”的html,如这里。虽然它不是严格的非侵入式js,但对于小型项目来说是可接受的

function start() {
  mydiv.innerHTML += "bar";
}

function sayHi() {
  alert("hi");
}
<body onload="start()">
  <div id="mydiv" style="border: solid red 2px">
    <span id="myspan" onclick="sayHi()">foo</span>
  </div>
</body>


1

还有另一种选择:使用setAttribute而不是添加事件监听器。像这样:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Demo innerHTML and event listeners</title>
<style>
    div {
        border: 1px solid black;
        padding: 10px;
    }
</style>
</head>
<body>
    <div>
        <span>Click here.</span>
    </div>
    <script>
        document.querySelector('span').setAttribute("onclick","alert('Hi.')");
        document.querySelector('div').innerHTML += ' Added text.';
    </script>
</body>
</html>


我不确定它是否适用于ES模块。 - FLAW
@FLAW - 我相信它确实如此。 - Frank Conijn - Support Ukraine
它可能会,但我指的是setAttribute。据我所知,属性就像字符串一样。因此,这基本上意味着您编写<button onclick="myfun()"></button>,如果myfun是模块中的函数,则无法访问它,除非将其设置为全局变量。建议,不要让你的保险变得更便宜。 - FLAW

0
最简单的方法是使用数组,将元素推入其中,然后动态地将数组后续值插入到数组中。 这是我的代码:
var namesArray = [];

function myclick(){
    var readhere = prompt ("Insert value");
    namesArray.push(readhere);
    document.getElementById('demo').innerHTML= namesArray;
}

0

在我看来,丢失事件处理程序是Javascript处理DOM的一个错误。为了避免这种行为,您可以添加以下内容:

function start () {
  myspan = document.getElementById("myspan");
  myspan.onclick = function() { alert ("hi"); };

  mydiv = document.getElementById("mydiv");
  clickHandler = mydiv.onclick;  // add
  mydiv.innerHTML += "bar";
  mydiv.onclick = clickHandler;  // add
}

2
我不认为这是一个 bug。替换元素意味着完全替换它。它不应该继承之前的内容。 - Diodeus - James MacFarlane
但是我不想替换元素;我只想将新元素添加到父级中。 - mike
如果您正在替换元素,我同意,@Diodeus。它不是事件处理程序的.innerHtml,而是.innerHtml的父级。 - Yes - that Jake.
2
innerHTML 的 owner 在其 innerHTML 更改时不会失去处理程序,只有其内容中的任何内容。而 JavaScript 并不知道您只是附加新子项,因为“x.innerHTML += y”仅是字符串操作“x.innerHTML = x.innerHTML + y”的语法糖。 - bobince
您不需要重新附加 mydiv.onclick。只有内部 span 的 onclick 被 innerHTML += 覆盖。 - Crescent Fresh

-1
你可以这样做:
var anchors = document.getElementsByTagName('a'); 
var index_a = 0;
var uls = document.getElementsByTagName('UL'); 
window.onload=function()          {alert(anchors.length);};
for(var i=0 ; i<uls.length;  i++)
{
    lis = uls[i].getElementsByTagName('LI');
    for(var j=0 ;j<lis.length;j++)
    {
        var first = lis[j].innerHTML; 
        string = "<img src=\"http://g.etfv.co/" +  anchors[index_a++] + 
            "\"  width=\"32\" 
        height=\"32\" />   " + first;
        lis[j].innerHTML = string;
    }
}

这个怎么避免丢失事件监听器? - Sebastian Simon

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