jQuery动态添加元素与事件委托导致重复执行的原理及解决方案

作者:交互手记

时间:2026-10-22

浏览:108

本文深入解析jQuery中使用on方法进行事件委托时,动态添加元素导致事件重复绑定的问题。通过测试代码揭示事件队列机制,指出根本原因在于事件绑定在父元素而非动态子元素。文章提供了包括使用off解绑、直接绑定、定时器清理及标志位控制在内的多种解决方案,并分析了各自的优缺点与潜在陷阱。

文章目录

前言与问题背景

前言

本文主要分享一个近期遇到的Bug,涉及jQuery的on方法绑定交互事件,例如类似$('#point').on('click','.read-more',function () {})的代码写法,这会导致程序逻辑重复执行。虽然许多文章提到使用off方法进行解绑,但往往未触及问题本质,即该现象的核心原因是事件委托机制所致。

直接看常见的几种代码写法:

第一种情况:


 $(document).on('click', function (e) {
 consol.log('jquery事件绑定')
 });

第二种情况:


 document.addEventListener('click',function (e) {
 consol.log('原生事件绑定')  
 });

第三种情况:


 var id = setInterval(function () {
 console.log('定时器循环事件绑定')
 },1000);

上述代码在开发中十分常见,看似简单的事件绑定,在SPA(单页应用)或AJAX局部刷新场景下,常引发意想不到的结果。

事件重复执行的原理分析

那么,什么是事件绑定导致的程序重复执行?要讲清楚这一点并不容易,以下通过一段测试代码进行说明,读者可自行拷贝本地验证:


<!DOCTYPE html>
<html>
<head>
 <meta charset="UTF-8">
 <title>Title</title>
</head>
<body>
<button class="add_but">点击</button>
<div id="point">fdfsdf
</div>
<script src="https://cdn.bootcss.com/jquery/1.8.3/jquery.js"></script> 
<script>
 var count=1;
 var example = {
 getData:function () {
  var data ={
  content:'df'+count++,
  href:''
  };
  this.renderData(data);
 },
 renderData:function (data) {
  document.getElementById('point').innerHTML='<div>this is a '+data.content+'点此<a class="read-more" href="javasript:;" rel="external nofollow" rel="external nofollow" >查看更多</a></div>';
  $('#point').on('click','.read-more',function () {
  alert('事故发生点');
 })
/*  setInterval(function () {
  console.log('fdfdfg');
  },2000);*/
  /*用冒泡来绑定事件,类似于Jquery的on绑定事件*/
 /* document.querySelector('body').addEventListener('click',function (e) {
  if(e.target.classList.contains('read-more')){
   alert('事故发生点');
  }
  })*/

 }
 } ;
 document.querySelector('.add_but').addEventListener('click',function (e) {
 example.getData();
 e.stopImmediatePropagation();
 });
</script>
</body>
</html>

以上测试代码旨在阐明问题。当点击页面按钮触发example.getData()函数,模拟AJAX获取数据成功后,会根据局部刷新后页面内类名为point的元素内容,为其中加载的read-more A标签绑定事件。效果看似正常:首次加载时,页面正常,‘事故发生点’弹出一次;但二次刷新触发后,弹窗出现两次;第三次则出现三次,依此类推。

这究竟是怎么回事?明明每次事件绑定前,之前绑定的元素都已被删除,为何被删除的元素仿佛仍在‘动作’?这是初次遇到此情况时的困惑。

最终请教专家后恍然大悟:绑定操作一直存在,且该绑定被保存在一个名为事件队列的地方,它并不在循环执行的主线程中。绘制了一张需要一定默契才能理解的示意图,供参考。

前言与问题背景

事件队列机制

还原真相

上述测试代码是特意为了演示而编写的。除了定时器外,若将两个点击事件改为正常写法,重复执行的情况便不会出现。正常的代码写法如下:


 // jquery 事件直接绑定的写法;
 $('#point .read-more').on('click',function () {
  alert('事故发生点');
 })
 // 原生JS 事件直接绑定的写法;
 document.querySelector('.read-more').addEventListener('click',function (e) {
  alert('事故发生点');
 })

看出区别了吗?关键在于不使用冒泡进行事件委托,而是直接为添加的元素绑定事件。因此,DOM事件是有逻辑的:动态添加元素并动态为此绑定事件,当元素被删除后,与其绑定的相应事件会从事件绑定队列中移除。这与上述测试代码给人的错觉不同,测试代码中给人的感觉是元素移除后,其绑定的事件仍残留在内存中。但请记住,这是一个误会。测试代码之所以产生这种错觉,是因为我们并未为动态添加的元素直接绑定事件,而是使用了事件委托形式。实际上,事件是绑定在#point元素上的,该元素一直存在,利用事件冒泡机制让程序感知到对动态添加链接元素的点击。测试中特意使用原生JS重现了这次事件委托,jQuery的on绑定事件原理基本相同。


document.querySelector('body').addEventListener('click',function (e) {
 if(e.target.classList.contains('read-more')){
  alert('事故发生点');
 }
})

解决方案与代码示例

解除Bug的那些方法

定时器方案

这是最容易犯的错误,也是最容易解决的错误。因为设定定时器时会返回一个数值,这应当是事件队列中该定时器的编号,类似9527。步骤是:设定一个全局变量来保存这个返回值id,在每次设定定时器时,先通过id清除已设定的定时器。


 clearInterval(intervalId); //粗暴的写法
 intervalId&&clearInterval(intervalId); //严谨的写法
 intervalId=setInterval(function () {
  console.log('fdfdfg');
  },2000); 

Dom事件方案

如前所述,最直接的办法是不采用事件委托,而是采用直接绑定。如果确实需要使用事件委托来绑定事件,则必须进行解绑。在jQuery中,提供了unbind函数来解绑事件,不过在jQuery 1.8版本以后,该方法已不推荐,转而推荐使用off方法。例如,对于上述on事件委托的方式,要解绑,可采用语句$('#point').off('click','.read-more')。

有缺陷的解决方案:添加flag标志

这很好理解,第一次绑定后,将flag置位,下次执行此绑定时,程序便知道该节点上已有绑定,无需再添加。具体操作如下:


 var flag = false;
 var example = {
 getData: function () {
  var data = {
  content: 'df' + count++,
  href: ''
  };
  this.renderData(data);
 },
 renderData: function (data) {
  document.getElementById('point').innerHTML = '<div>this is a ' + data.content + '点此<a class="read-more" href="javasript:;" rel="external nofollow" rel="external nofollow" >查看更多</a></div>';
  !flag && $('#point').on('click', '.read-more', function () {
  alert('事故发生点'+data.content);
  });
  flag = true;
 }
 };

从逻辑上看似乎没有问题,但仔细观察会发现存在缺陷。当我们第二次、第三次刷新时,弹出框的内容仍与第一次模拟刷新后点击弹出的内容一致,即仍是'事故发生点df1',而非随内容递增。这是为何?感觉事件队列里面的回调函数被单独保存起来了,data被深拷贝了,而不再是一个引用。这确实有点难理解,具体原因尚不明确,若有高手能解释清楚,请务必告知。

最后总结,其实平常编写程序时,事件绑定造成程序重复执行的情况很少发生。这通常出现在编写插件时,因为插件需要适应多种调用环境,所以在插件内部做到防止事件重复绑定的情况非常重要。

总结

以上就是这篇文章的全部内容,希望本文的内容对大家的学习或者工作具有一定的参考学习价值。如果有疑问大家可以留言交流,谢谢大家对本站的支持。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章