数字营销 · Web开发 · 基础设施

HTML中动态生成内容的事件绑定问题

分析动态生成 HTML 元素无法直接绑定事件的问题,并通过事件冒泡和事件委托解决动态内容的事件绑定。

由于实际的需要,有时候我们需要向网页中动态插入 HTML 内容,并且给新插入的节点绑定事件处理函数。我们知道,使用 JavaScript 向 HTML 文档中插入内容,大致有两种方法。

一种是在 JavaScript 中直接生成 HTML,然后插入到文档中。

另外一种是通过 Ajax 从服务器获取数据,再使用 JavaScript 对获取到的数据进行处理,然后插入文档。

这两种方法各有特点。本文主要分析一个问题:

动态插入到文档中的元素,怎么样绑定事件?

这里假定新插入的对象不使用内联事件绑定,也就是不使用类似:

<a onclick="...">

这样的方式。下面分别使用 jQuery 和原生 JavaScript 来进行说明。

情况一:HTML代码存放在JavaScript中

先来看下面的代码:

<body>

<p>第1行内容</p>
<p>第2行内容</p>
<p>第3行内容</p>

<script>

var appendhtml = document.createElement("p");

appendhtml.innerHTML = "这是插入的内容";

document.body.appendChild(appendhtml);


var nodep = document.getElementsByTagName("p");

for(var i = 0; i < nodep.length; i++){

nodep[i].onclick = function(){
console.log("Click Event!");
}
}

</script>

</body>

上面的代码使用原生 JavaScript 动态生成了第四个 p 标签。由于生成 p 标签的代码在前面,而绑定事件的代码在后面,所以当执行绑定事件的代码时,页面中已经存在 4 个 p 元素。因此,点击这 4 个 p 标签时,都会触发相应的事件。那么,是不是说明:

只要是 JavaScript 动态生成的 HTML 内容,都可以直接绑定事件?

答案是否定的。上面的 script 中实际上有两段代码。

第一段负责生成内容:

document.body.appendChild(appendhtml);

第二段负责绑定事件:

var nodep = document.getElementsByTagName("p");

for(var i = 0; i < nodep.length; i++){
...
}

如果把这两段代码的顺序交换一下,会发现 JavaScript 动态生成的第四个 p 就没有绑定上 click 事件。

用 jQuery 测试也是一样:

$(function(){

$("p").click(function(){
console.log("Click Event");
});

$("<p>这是生成的内容</p>").appendTo("body");

});

运行之后,原来的 p 可以触发事件,但是后来动态生成的 p 并没有绑定上。由此可以看到,代码执行的先后顺序和事件绑定是否成功有直接关系。

其实这个问题很好分析。无论是:

document.getElementsByTagName("p")

还是 jQuery 的:

$("p")

本质上都是在查找当前页面中已经存在的元素。如果元素还没有被插入到页面中,那么选择器自然无法找到它,也就没有办法直接给它绑定事件。

动态加载的内容怎么办?

但是实际工作中,后生成的元素往往也需要绑定事件。例如网站的留言系统。第一次加载页面时,只显示固定数量的评论。如果评论超过一定数量,剩下的评论通过 Ajax 动态加载。而每条留言最后都有一个“回复”功能,点击之后可以回复对应的留言。这样一来,Ajax 动态加载进来的留言同样需要绑定 click 事件,并注册回复留言的函数。当然,最直接的办法就是:

每次 Ajax 加载完成之后,再重新绑定一次 click 事件。

但是这样会产生重复代码,也会增加一些额外的处理,而且代码维护起来也比较麻烦。那么有没有更好的办法呢?

留言系统的回复功能

这里可以换一个角度理解。无论 HTML 内容是不是 JavaScript 动态生成的,只要它已经属于当前页面,就可以参与 DOM 事件处理。

JavaScript 中有一个非常重要的概念:

事件冒泡。

简单来说,一个子元素产生事件之后,事件会不断向父元素传播。例如:

p

div

body

document

所以我们可以换一种思路:

不直接给未来才会出现的元素绑定事件,而是在它已经存在的父元素上监听事件。

这样,当未来生成的元素触发事件时,事件仍然可以冒泡到父元素,然后由父元素进行处理。

事件冒泡

使用jQuery的delegate

来看下面的代码。注意,这里插入内容的代码依然放在后面。

$(function(){
$("body").delegate(
"p",
"click",
function(){
console.log("Click Event");
}
);
$("<p>这是生成的内容</p>").appendTo("body");
});

这时候发现,新生成的 p 元素点击之后也能够正常输出:

Click Event

说明这种方式是可以正常工作的,这里使用的是 jQuery 的:

delegate()

来看一下官方当时的解释:

Attach a handler to one or more events for all elements that match the selector, now or in the future, based on a specific set of root elements.

简单来说,就是:

以指定的根元素为基础,把事件绑定到符合条件的元素上,不管这些元素现在是否存在,还是以后才生成。

在当时使用的 jQuery 版本中,on() 方法同样可以完成类似的事件委托操作,这里就不再详细说明。

原生JavaScript怎么实现?

jQuery 已经提供了比较方便的 API,那么原生 JavaScript 应该怎么实现呢?

下面给出一个比较简单的方案:

function gelegate(action, selector, callback){
document.addEventListener(action, function(e){
if(
selector == e.target.tagName.toLowerCase() ||
selector == e.target.className
){
callback();
}
});
}


gelegate("click", "p", function(){
console.log("Click Event");
});


var appendhtml = document.createElement("p");
appendhtml.innerHTML = "这是插入的内容";
document.body.appendChild(appendhtml);

这里的思路也是一样的。我们没有直接给 p 元素绑定事件,而是给 document 绑定了 click 事件。当 p 元素被点击之后,事件会向上冒泡到 document。

然后通过:

e.target

判断最终触发事件的对象是不是我们需要的元素。如果符合条件,就执行回调函数。这样,即使 p 元素是在事件绑定之后才动态生成的,也能够触发对应的处理函数。

不过需要注意,这段代码只是一个简单的示例,当时主要用于说明事件冒泡和事件委托的原理,实际项目中还需要考虑更多兼容性问题。

评论1

欢迎分享你的看法,也欢迎补充不同的实践经验。

N
Noeltouch
十分感谢作者!!正好遇到了这个难题,使用简单的一行父元素绑定的代码,马上就成功了!!