阅读 70

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

事件模型

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

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

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

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

关于事件模型的演示:

<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 内的文本

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

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

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 属性更加保险。

关注下面的标签,发现更多相似文章
评论