由于实际的需要,有时候我们需要向网页中动态插入 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
欢迎分享你的看法,也欢迎补充不同的实践经验。