由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。

这里摘用一下凌云之翼对事件委托的例子。

有三个同事预计会在周一收到快递。为签收快递,有两种办法:一是三个人在公司门口等快递;二是委托给前台MM代为签收。现实当中,我们大都采用委托的方案(公司也不会容忍那么多员工站在门口就为了等快递)。前台MM收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款。这种方案还有一个优势,那就是即使公司里来了新员工(不管多少),前台MM也会在收到寄给新员工的快递后核实并代为签收。 这里其实还有2层意思的: 第一,现在委托前台的同事是可以代为签收的,即程序中的现有的dom节点是有事件的; 第二,新员工也是可以被前台MM代为签收的,即程序中新添加的dom节点也是有事件的。

事件委托的优点

省监听的内存

如果要给100个按钮添加点击事件,我们只需要用事件委托监听这100个按钮的祖先,等冒泡的时候判断target是不是这100个按钮中的一个,这样我们就需要一个内存空间就够了,自然性能也会更好。

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title>JS Bin</title>
  6. </head>
  7. <body>
  8. <div id="div1">
  9. <button data-id="1">click 1</button>
  10. <button>click 2</button>
  11. <button>click 3</button>
  12. //...
  13. <button>click 100</button>
  14. </div>
  15. </body>
  16. </html>
  1. div1.addEventListener('click', (e) => {
  2. const t = e.target
  3. if (t.tagName.toLowerCase() === 'button') {
  4. console.log('button 被点击了')
  5. console.log('button 内容是' + t.textContent)
  6. console.log('button data-id是' + t.dataset.id) //dataset获取以data开头的属性的值
  7. }
  8. })

可以监听动态元素

如果有一个button在一秒钟之后出现,那我们该怎么监听他,监听祖先,等点击的时候看看是不是我想要监听的元素即可。

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title>JS Bin</title>
  6. </head>
  7. <body>
  8. <div id="div1">
  9. </div>
  10. </body>
  11. </html>
  1. setTimeout(() => {
  2. const button = document.createElement('button')
  3. button.textContent = 'click 1'
  4. div1.appendChild(button)
  5. }, 1000)
  6. div1.addEventListener('click', (e) => {
  7. const t = e.target
  8. if (t.tagName.toLowerCase() === 'button') {
  9. console.log('button 被click')
  10. }
  11. })

封装事件委托

写出这样一个函数on('click','#testDiv','li',fn),当用户点击#testDiv里的li元素时,调用fn函数,要求用到事件委托。

答案一

判断target是否匹配’li’

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title>JS Bin</title>
  6. </head>
  7. <body>
  8. <div id="div1">
  9. </div>
  10. </body>
  11. </html>
  1. setTimeout(() => {
  2. const button = document.createElement('button')
  3. button.textContent = 'click 1'
  4. div1.appendChild(button)
  5. }, 1000)
  6. //在div1上做事件委托看button有没有被点击
  7. on('click', '#div1', 'button', () => {
  8. console.log('button 被点击了')
  9. })
  10. //输入(事件类型,元素,选择器/准备匹配什么元素,回调函数)
  11. function on(eventType, element, selector, fn) {
  12. if (!(element instanceof Element)) {
  13. element = document.querySelector(element)
  14. }
  15. element.addEventListener(eventType, (e) => {
  16. const t = e.target
  17. //判断一个元素是否满足一个选择器
  18. if (t.matches(selector)) {
  19. fn(e)
  20. }
  21. })
  22. }

答案二

如果button里面还有个span,span的内容是chick 1,点击chick 1 实际操作的元素是span,就不能通过button被点击了。
要用递归判断,如果当前元素不匹配button,就向自己的祖辈寻找,如果祖辈里有button就说明点击了button。

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title>JS Bin</title>
  6. </head>
  7. <body>
  8. <div id="div1">
  9. </div>
  10. </body>
  11. </html>
  1. setTimeout(() => {
  2. const button = document.createElement('button')
  3. const span = document.createElement('span')
  4. span.textContent = 'click 1'
  5. button.appendChild(span)
  6. div1.appendChild(button)
  7. }, 1000)
  8. //在div1上做事件委托看button有没有被点击
  9. on('click', '#div1', 'button', () => {
  10. console.log('button 被点击了')
  11. })
  12. //输入(事件类型,元素,选择器/准备匹配什么元素,回调函数)
  13. function on(eventType, element, selector, fn) {
  14. if (!(element instanceof Element)) {
  15. element = document.querySelector(element)
  16. }
  17. element.addEventListener(eventType, e => {
  18. let el = e.target
  19. //看看被操作的元素是否符合button
  20. while (!el.matches(selector)) {
  21. //如果在找的时候一直找到了div1,就认为找不到了,判定为空,跳出
  22. if (element === el) {
  23. el = null
  24. break
  25. }
  26. //不符合就让这个元素等于父元素
  27. el = el.parentNode
  28. }
  29. //如果找到了就调用fn,第一个参数传e,第二个传匹配到的元素
  30. el && fn.call(el, e, el)
  31. })
  32. return element
  33. }