DOM事件机制和事件委托

DOM事件机制

引入

从下面代码引入我们今天的话题

    <div class="爷爷">
      <div class="爸爸">
        <div class="儿子">
              文字
        </div>
      </div>
    </div>
复制代码

对于上述代码,给三个 div 分别添加事件监听 fnYe、fnBa、fnEr。

提问 1:点击了谁?

  • 点击文字,算不算点击爷爷
  • 点击文字,算不算点击爸爸
  • 点击文字,算不算点击儿子

答案:都算。

提问 2:调用顺序

点击文字,最先调用 fnYe/fnBa/fnEr 中的哪一个函数?

答案:都行。IE 认为先调用 fnEr,网景认为先调用 fnYe。

W3C 事件模型

直到 2002 年,W3C 发布了标准。规定了浏览器应该同事支持两种调用顺序,首先按照爷爷=>爸爸=>儿子的顺序看有没有函数监听,然后按照儿子=>爸爸=>爷爷的顺序看有没有函数监听,有监听函数就调用,并且提供事件信息,没有就直接跳过。

先捕获再冒泡,注意 e 对象被传给所有监听函数,事件结束后,e 对象就不存在了。

那么这里还存在一个问题:难道 fnYe、fnBa、fnEr 都要调用两次了,当然不是的,这里由开发者自己选择是吧 fnYe 放在捕获阶段还是冒泡阶段

DOM 事件机制分为捕获阶段和冒泡阶段。

那么我们上边提到的捕获和冒泡是什么呢?

事件捕获:简单来说就是从外向内找监听函数。例如上述代码中,事件捕获的监听顺序就是 fnYe>fnBa>fnEr。

事件冒泡:简单来说就是从内向外找监听函数。例如上述代码中,事件冒泡的监听顺序就是 fnEr>fnBa>fnYe。

DOM 事件机制的示意图如下图所示,可以帮助我们简单理解捕获和冒泡。
DOM 事件机制

我们再来讨论一下捕获和冒泡的顺序,按照 WC3 的标准,我们可以得出先捕获后冒泡的这样一个顺序。但是,有一个特例,不是这样的。

当只有一个元素被监听的时候,(不考虑父子同时被监听),并且 fn 分别在捕获阶段和冒泡阶段同时监听 click 事件,这种情况下,用户点击的元素就是开发者监听的元素,谁先监听那么谁就先执行。下面两张图可供参考。

1.png

2.png

取消冒泡

知道了捕获和冒泡的顺序,我们再来看看如何取消冒泡(捕获是不能取消的)。

使用 e.stopPropagetion 就可以中断冒泡。但是有些事件是不能阻止默认动作的,例如 scroll event(滚轮事件)。MDN 搜索 scroll event 可以看到下图的内容,Bubbles 是指该事件是否冒泡,所有的冒泡都可以取消。Cancelable 是指开发者是否可以阻止默认事件,与冒泡无关。

image.png

事件委托

事件委托:通过监听一个祖先元素,同时操作多个后代。

场景一:
我们要给 100 个按钮添加点击事件,
怎么办?

最笨的办法:直接给 100 个按钮都 addEventListener。

有了事件委托后:监听这 100 个按钮的爸爸,等冒泡的时候,判断 target 是不是这 100 个按钮中的一个

场景二:
我们要监听目前不存在的元素的点击事件,咋办?

有了事件委托:监听祖先,等到冒泡时,判断点击的元素是不是我想要监听的元素。


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

html 代码如下:

    <div id="test">
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
    </div>
复制代码

JS 代码实现如下:

    test.addEventListener('click',(e)=>{
        const t = e.target
        if(t.tagName.toLowerCase()==='li'){ //判断被操元素tagName是不是li
          console.log('li 被点击了')
          console.log('点击内容是'+t.textContent)
        }
    })
复制代码

上述代码实现思路:

  1. 首先监听父元素。
  2. 然后根据浏览器传进去的事件信息,拿到当前点击元素。
  3. 再判断当前点击元素是不是li元素,如果是,就 console.log(‘用户点击 li 标签’)

所以,我们就可以封装一个事件委托函数。

    on('click', '#test', 'li', ()=>{
        console.log('用户点击了 li')
            })

    function on(eventType, element, selector, fn){
      if(!(element instanceof Element)){ //判断是不是element
          element = document.querySelector(element)
         }
      element.addEventListener(eventType,(e)=>{
        const t = e.target
        if(t.matches(selector)){
            fn(e)
            console.log('点击内容是'+t.textContent)
       }
  })
}
复制代码

但是,上面这个委托函数还是存在问题的。

如果,给 li 添加 span,代码如下:

    <ul id="test">
      <li>
        <span>1</span>
      </li>
      <li>
        <span>2</span>
      </li>
      <li>
        <span>3</span>
      </li>
      <li>
        <span>4</span>
      </li>
    </ul>
复制代码

此时,上面的委托函数就不适用了,因为此时的被操的 e.target 是 span 而不是 li 了,就必须使用下面的委托函数。

    function on(element, eventType, selector, fn) {
      element.addEventListener(eventType, e => {
        let el = e.target
        while (!el.matches(selector)) {
          if (element === el) {
            el = null
            break
          }
          el = el.parentNode
        }
        el && fn.call(el, e, el)
      })
      return element
    }
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享