js事件委托

1.js事件委托是什么

js里的事件委托,简而言之,就是将原本应该在当前元素绑定的事件,放到它的祖先元素上,让祖先元素来委托处理。
再通俗点:就是自己的事不想干,叫它爸爸,甚至爷爷、甚至祖先来干。

真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。

js事件委托作用:
1).节约内存,减少内存消耗
试想一下,如果我们有一个列表,列表之中有大量的列表项,我们需要在点击列表项的时候响应一个事件。如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的,效率上需要消耗很多性能。因此,比较好的方法就是把这个点击事件绑定到他的父层,然后在执行事件的时候再去匹配判断目标元素。所以事件委托可以减少大量的内存消耗,节约效率。

2).动态绑定事件,能为之后新增的DOM元素依然添加事件(如:js动态添加li)
比如上述的例子中列表项就几个,我们给每个列表项都绑定了事件。在很多时候,我们需要通过 AJAX 或者用户操作动态的增加或者去除列表项元素,那么在每一次改变的时候都需要重新给新增的元素绑定事件,给即将删去的元素解绑事件。如果用了事件委托就没有这种麻烦了,因为事件是绑定在父层的,和目标元素的增减是没有关系的,执行到目标元素是在真正响应执行事件函数的过程中去匹配的。所以使用事件在动态绑定事件的情况下是可以减少很多重复工作的。

前面提到 DOM 中事件委托的实现是利用事件冒泡的机制,那么事件冒泡是什么呢?

2.事件流或事件传播

事件流指从页面中接收事件的顺序,也可理解为事件在页面中传播的顺序。

事件的传播分为三个阶段:捕获阶段、目标阶段、冒泡阶段。
1).捕获阶段:从window,document 和根元素开始,事件向下扩散至目标元素的祖先
2).目标阶段:事件在用户单击的元素上触发
3).冒泡阶段:最后,事件冒泡通过目标元素的祖先,一直到根元素 document 和 window。

我们通常用 addEventListener 给元素添加事件。

document.querySelector('#card')addEventListener(
  'click',
  function (event) {
   console.log('div#card 冒泡点击', event);
 },
  false
);

第一个参数是事件名。
第二个参数是事件响应函数,可以拿到当前的事件对象。
第三个参数是可选的,表示监听的是否为捕获阶段,由addEventListener(ev, fn, useCapture = false)第三个参数控制,默认值false,当useCapturefalse,表示该事件在冒泡阶段触发,当useCapturetrue,表示该事件在捕获阶段触发。

调用事件对象的 event.composedPath() 方法可以拿到事件路径的顺序。

假设我们的 DOM 结构如下:

<html>
  <head>
    <title>测试</title>
    <meta charset="UTF-8" />
  </head>

  <body>
    <div id="app">
      <div id="box-1">
        <div id="card">card</div>
      </div>
      <div id="box-2"></div>
    </div>
  </body>
</html>

现在我们点击 card 文字时,DOM 就会产生事件流。

事件流首先会进入 捕获阶段,从根节点往目标元素(div#card)移动,依次经过为:

window
document(文档根元素,在 HTML 中没有显式声明)
document.documentElement(<html> )
document.body(<body>)
...
目标元素 div#card

和调用事件对象的 event.composedPath() 方法拿到的 事件路径 类似。

window 看起来是个全局变量,但它也是可以绑定事件的,比如窗口大小改变的 resize 事件就只能绑定到 window 上,而不能绑定到 document 上。

然后再执行 冒泡阶段,然后反着再经过一遍这些节点。

我们会根据事件流经过的顺序,依次执行这些节点上绑定的对应事件函数。

3.应用场景
应用场景1:

<ul>
  <li>小王<button data-user-id="5">聊天</button></li>
  <li>小明<button data-user-id="99">聊天</button></li>
  <!-- ... -->
  <li>老王<button data-user-id="63">聊天</button></li>
</ul>
<script>
document.querySelector('ul').addEventListener('click', (event) => {
  const target = event.target;
  const userId = target.getAttribute('data-user-id');
  if (userId) {
    joinChat(userId);
  }
});
</script>

代码解析:
将事件绑定到 ul 节点上,执行函数时,通过 event 对象拿到必要的信息,进行统一的操作。通过 event.target 我们能获得这次事件流的目标节点,然后从该节点对象中提取出需要的信息。这样,不管 li 有多少,更新多频繁,我们只需要维护一个函数就够了。

应用场景2:

<html>
<head>
    <meta charset="UTF-8">
    <title>demo</title>
</head>
<body>
<ul>
    <li>item1</li>
    <li>item2</li>
    <li>item3</li>
    <li>item4</li>
    <li>item5</li>
</ul>
<script type="text/javascript">
    // 找到所有的li
    var liList = document.getElementsByTagName('li');
    // 遍历所有的li,并给每个li添加点击事件
    for (var i=0;i<liList.length;i++){
        liList[i].onclick = function () {
            alert(this.innerHTML);
        }
    }
</script>
</body>
</html>

代码解析:
给5个li标签加了点击事件,当界面上点击li时,会打印它们各自li标签显示的内容。

出现的问题:此时5个li,看起来每个li的点击事件触发时调用的都是同一个函数,即:

function () {
    alert(this.innerHTML);
}

但其实并不是这样。每个li绑定的都是一个全新的函数,只不过每个函数的样子都一模一样。

如何验证这个结论呢?很简单,判断每个li标签的onclick是否相等就可以了。

代码验证如下:

alert(liList[0].onclick==liList[1].onclick);
//弹出false

至于上面说的函数长得一样,但不是同一个数据这种情况就类似于 var obj1 = {name:"jack",age:16}var obj2 = {name:"jack",age:16},obj1 == obj2得到的结果也会是false

至此,我们可以得到结论,如果有5个li,那么就有5个函数会被创建在内存中占据空间,那如果有100个li呢?就会有100个长相一模一样的函数在内存中常驻,对内存的开销是巨大的!

解决办法:利用事件冒泡的原理,把事件加在父元素(ul)身上!
代码如下:

var ul = document.getElementsByTagName('ul')[0];
// 只用给ul加点击事件即可
ul.onclick = function (e) { //事件对象在ie8中要通过window.event才能取到,因此e = e || window.event是做兼容处理
    e = e || window.event;
    // e.target指的是被点击的那个li
    alert(e.target.innerHTML);
}

原理解析:

回顾事件冒泡

事件冒泡:即一个元素的事件触发后,会依次一级一级往上调用父级元素的同名事件,直到window(注:IE8和之前的浏览器只到document)

例:div > p > span 当div和p以及span都添加了click事件,当点击span时,会依次向上触发span、p、div的click事件。其中,在每个触发的事件里,通过事件对象.target能拿到触发事件的源头元素,也就是 事件源alert(e.target),结果都是[object HTMLSpanElement].

在回顾完事件冒泡后,我们显而易见得到结论:给所有li添加点击事件,只要加到它们的父元素ul身上的根本原因是利用了事件冒泡。也即:无论点击哪个li,都会自动触发ul的点击事件,然后在ul里通过e.target能获得真正被点击的那个li,继而拿到它的innerHTML

小结:如果给一堆元素加事件,并且事件触发时执行的代码都差不多时,就可以把事件加在父元素身上啦!
看,这样的形式是不是就相当于把自己的事件,委托在父元素身上处理了呢?因而它才会叫事件委托!

思考:如果ul里还有其他子元素例如span,可我只想给li加点击事件,用原来写的事件委托还行吗?

答案是否定的,因为根据事件冒泡原理,所有子元素点击后都会触发父元素的点击,因此,如果你点击了span,也会调用ul的点击事件,这就相当于给span也加了点击事件。这时候该怎么解决呢?

很简单,只要判断一下事件源是不是li就行了,如果是li才执行代码,否则不执行,代码如下:

<html>
<head>
    <meta charset="UTF-8">
    <title>demo</title>
</head>
<body>
<ul>
    <li>item1</li>
    <li>item2</li>
    <li>item3</li>
    <li>item4</li>
    <li>item5</li>
    <span>我是span</span>
</ul>
<script type="text/javascript">
    var ul = document.getElementsByTagName('ul')[0];
    // 只用给ul加点击事件即可
    ul.onclick = function (e) {
        e = e || window.event;
        // 判断事件源是不是li
        if(e.target.nodeName.toLowerCase()=='li'){
            // e.target指的是被点击的那个li
            alert(e.target.innerHTML);
        }
    }
</script>
</body>
</html>

jQuery事件委托语法:

$('父元素').on('事件名','哪个子元素触发',传给回调函数的参数,事件触发时的回调函数);

解释:
1).参数1:事件名,代表要绑定什么事件,但是记得不用加on,也就是说如果你想加点击事件,只要写’click’即可,注意是字符串!所以要打单引号或者双引号
2).参数2:只能由哪个子元素触发,例如我写 “li”,就代表只能由这个父元素里面的li触发事件,其他子元素不会触发。需要注意的是,这也是字符串,并且,参数2可以不写,那就代表仅仅只是给父元素加一个点击事件,并且所有子元素都能触发到这个事件(因为事件冒泡)
3).参数3:其实一般不会用,就是如果想事件触发时,自己给回调函数传一些值就写,这个参数也可以不写!
4).参数4:事件触发时的回调函数

总结:参数1和参数4是必须的,其他是可选的,如果你要用事件委托,请写上参数2

$('ul').on('click','li','我是数据',function (e) {
    console.log(e.data);
    console.log(e.target.innerHTML);
    // 或者
    console.log(this.innerHTML);
})

说明:
1.on这个方法的参数3可以通过回调函数里的e.data拿到(但一般不会用,大家了解一下有这么个东西即可)
2.在jQuery事件委托的回调函数里this和e.target是同一个东西,但是在JS里this和e.target不是同一个东西

出处:www.l1mn.com

原文标题:js事件委托

原文地址:https://www.l1mn.com/p/wquaa1.html

本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。

评论

皖ICP备2023023451号

Copyright © L1MN.COM 联系方式:l1mnfw@163.com