用例子解释事件模型和事件代理

事件模型

事件传播模型 在说事件代理以前,先来讲一下事件模型。
在浏览器开发的早期,面对事件触发模型的问题,全部的程序员都认为事件触发不该该是直接触发的,而应该在文档中有一个传播的过程,然而事件传播的顺序应该是什么样的?
当时的程序员分为两个派别:javascript

  • 以微软程序员为主的事件捕获派
  • 以其余公司程序员主导的事件冒泡派

因而,微软表明的事件捕获派制做出了支持dom事件捕获的 IE 浏览器。而事件冒泡派则制做出了如 Firefox 这样支持事件冒泡的浏览器。
双方意见相左,标准不一。后来,W3C横空出世,收编两方意见,给了一个统一的标准,就是如今的事件模型。
在 W3C 的标准中,事件捕获和事件冒泡都是合乎规范的,开发者能够本身指定事件的传播模型。
那么,什么是事件捕获,什么是事件冒泡,有必要争论吗?css

事件捕获:触发一个事件时,从DOM树的最顶层开始寻找事件监听函数,若找到相对应事件的监听函数,则当即执行该函数,而后继续向下寻找, 直到寻找到触发事件的那个元素为止。
事件冒泡:与事件捕获相反,事件冒泡认为事件触发以后,应该从触发事件的元素往DOM树的上层传播,向上寻找相对应事件监听函数,一样是找到执行,以后继续寻找,直到DOM树的顶端。
因为事件冒泡更符合人的理解,现代浏览器(如Chrome)默认支持事件冒泡,只有远古时代的IE支持事件捕获。固然,在绑定事件时,能够指定事件传播模型。html

关于事件模型的演示:java

<body>
<div class="red">
  <div class="blue">
    <div class="green">
      <div class="yellow">
        <div class="orange">
          <div class="purple">
            
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
</body>
复制代码
*{margin:0;padding:0;box-sizing:border-box;}
.red.active {
  background: red;
}
.blue.active {
  background: blue;
}
.green.active {
  background: green;
}
.yellow.active {
  background: yellow;
}
.orange.active {
  background: orange;
}
.purple.active {
  background: purple;
}
div {
  border: 1px solid black;
  padding: 10px;
  transition: all 0.5s;
  display: flex;
  flex:1;
  border-radius: 50%;
  background: white;
}

.red{
  width: 100vw;
  height: 100vw;
}
复制代码
// 捕获模型,先捕获,后冒泡。
let divs = $('div').get()
let n = 0
for (let i = 0; i < divs.length; i++) {
  divs[i].addEventListener('click', () => {
    setTimeout(() => {
      divs[i].classList.add('active')
    }, n * 500)
    n += 1
  }, true)
}


for (let i = 0; i < divs.length; i++) {
  divs[i].addEventListener('click', () => {
    setTimeout(() => {
      divs[i].classList.remove('active')
    }, n * 500)
    n += 1
  })
}
复制代码
// 冒泡模型,省略捕获,直接冒泡。
let divs = $('div').get()
let n = 0
for (let i = 0; i < divs.length; i++) {
  divs[i].addEventListener('click', () => {
    setTimeout(() => {
      divs[i].classList.add('active')
    }, n * 500)
    n += 1
  }, false)
}


for (let i = 0; i < divs.length; i++) {
  divs[i].addEventListener('click', () => {
    setTimeout(() => {
      divs[i].classList.remove('active')
    }, n * 500)
    n += 1
  })
}
复制代码

既然事件是具备传播性的,那么,能不能利用这个特性搞点事情呢?程序员

事件代理

事件代理的原理:利用事件模型的传播性质,将子元素的监听函数绑定到父元素上,经过事件传播去执行监听函数。浏览器

####场景: 假设如今有一个 ul 元素,里面有 4 个 li 子元素,须要给4个子元素添加一个鼠标点击事件,log 出 li 内的文本app

<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
</ul>
复制代码

常规的方式是直接添加事件监听:dom

let ul = document.querySelector('ul')
let lis = ul.querySelectorAll('ul li')

for(let i = 0; i < lis.length; i++){
  lis[i].addEventListener('click',(e)=>{
    console.log(e.currentTarget.textContent)
  })
}
// 获取 li 元素,遍历全部 li 并给 li 添加事件监听
复制代码

在这种方法中,每个元素都添加了1个事件监听,一共添加了4个事件监听,内存占用较大。函数

接下来,需求要求添加一个 li 元素,并一样添加事件监听。因而,这样解决性能

let li = document.createElement('li')
li.textContent = 5
li.addEventListener('click',(e)=>{
    console.log(e.currentTarget.textContent)
  })
ul.appendChild(li)
// 建立一个新的 li 元素,并给该 li 元素添加事件监听
复制代码

目前,一共有 5 个事件监听了,占用内存又大了一些。
那么,你有没有考虑过,万一是给 10000 个 li 元素添加监听事件呢?那不就有 10000 个事件监听了?万一要新加 10000 个新元素呢?那不是要从新加 10000 个事件监听?
怎么解决上面说的这种问题?
使用事件代理:

let ul = document.querySelector('ul')
let lis = ul.querySelectorAll('ul li')

ul.addEventListener('click',(e)=>{
  console.log(e.target.textContent)
}) // 将全部子元素的事件代理到父元素上

let li = document.createElement('li')
li.textContent = 5
ul.appendChild(li)
// 直接添加新元素,新元素的事件一样会被代理
复制代码

使用事件代理以后,不管有多少个子元素,都只有一个事件监听,同时,效果也是同样的,节约了内存。在增长新元素时,也不用再修改事件绑定。

优势:

  • 提升JavaScript性能。将子元素同一类型的事件监听绑定到父元素上,只声明了一个监听函数,减小了内存的占用,提升响应速度。
  • 方便动态添加DOM元素。使用事件代理以后,用JS动态添加子元素时,不须要由于元素改动而修改事件绑定。

target 和 currentTarget

使用事件代理的一个问题是须要分清楚 target 和 currentTarget 两个属性,在适当的时候选择适当的属性。

一个触发事件的对象的引用。它与event.currentTarget不一样, 当事件处理程序在事件的冒泡或捕获阶段被调用时。————MDN

当事件遍历DOM时,标识事件的当前目标。它老是引用事件处理程序附加到的元素,而不是event.target,它标识事件发生的元素。————MDN

MDN上对于 target 和 currentTarget 的描述有点难以理解。
实际上,target 就是触发事件的元素自己,不必定是绑定事件监听的元素。而 currentTarget 则必定是绑定事件监听的元素,不必定是触发事件的元素。 代码演示:

<ul>dsfasdf
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
  </ul>
复制代码
let ul = document.querySelector('ul')

ul.addEventListener('click',(e)=>{
  console.log('我打印出的是target的值:' + e.target.textContent)
  console.log(e.target.textContent)
  console.log('我打印出的是currentTarget的值')
  console.log(e.currentTarget.textContent)
  if(e.target === e.currentTarget){
    console.log(1)
  }
},false)

// 点击第4个li
// 控制台将会打印出: 
// 我打印出的是target的值:4
// 我打印出的是currentTarget的值:dsfasdf
// 1
// 2
// 3
// 4

复制代码

看起来控制台打印出了 6 行,那么是否是因此的 li 都会被冒泡到呢?其实不是。
实际上,控制台只打印了 2 行,1 行是点击的 4,另外一行是整个 ul ,因此全部元素都被打印出来了。

当绑定事件监听的元素和触发事件的元素是同一个时,target === currentTarget。
在上面的例子中,就是点击 ul 时,target 才等于 currentTarget。
因此使用事件代理,必须使用 target,不能使用 currentTarget。

当一个事件处理函数绑定到多个元素上时,因为冒泡和捕获机制的存在,使用target可能会错误触发不想触发的元素,因此使用 currentTarget 属性更加保险。

相关文章
相关标签/搜索