彻底捋一捋JS事件:捕获与冒泡、事件处理程序、事件对象、跨浏览器、事件委托

3,414 阅读6分钟
原文链接: github.com

一、捕获与冒泡

事件流描述的是从页面中接收事件的顺序
IE 的事件流是事件冒泡流
而 Netscape Communicator 的事件流是事件捕获流

DOM2级事件规定的事件流包括三个阶段:

  • 事件捕获阶段
  • 处于目标阶段
  • 事件冒泡阶段
    首先发生的是事件捕获,为截获事件提供了机会。
    然后是实际的目标接收到事件。
    最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应
    a192fc27-28d8-46c6-8400-fd3831e239c5

画重点(这部分内容可以先跳过,看完下面的内容再回头消化)
1、当处于目标阶段,没有捕获与冒泡之分,执行顺序会按照addEventListener的添加顺序决定,先添加先执行

2、使用stopPropagation()取消事件传播时,事件不会被传播给下一个节点,但是,同一节点上的其他listener还是会被执行

// list 的捕获
$list.addEventListener('click', (e) => {
  console.log('list capturing');
  e.stopPropagation();
}, true)
  
// list 捕获 2
$list.addEventListener('click', (e) => {
  console.log('list capturing2');
}, true)

// list capturing
// list capturing2

如果想要同一层级的listener也不执行,可以使用stopImmediatePropagation()

3、preventDefault()只是阻止默认行为,跟JS的事件传播一点关系都没有

4、一旦发起了preventDefault(),在之后传递下去的事件里面也會有效果

二、事件处理程序

共有三种事件处理程序:DOM0、DOM2、IE

var btn = document.getElementById('btn');

btn.onClick = () => {
  console.log('我是DOM0级事件处理程序');
}
btn.onClick = null;

btn.addEventListener('click', () => {
  console.log('我是DOM2级事件处理程序');
}, false);
btn.removeEventListener('click', handler, false)

btn.attachEvent('onclick', () => {
  console.log('我是IE事件处理程序')
})
btn.detachEvent('onclicn', handler);

画重点:

DOM2级的好处是可以添加多个事件处理程序;DOM0对每个事件只支持一个事件处理程序

通过DOM2添加的匿名函数无法移除,上面写的例子就移除不了,addEventListenerremoveEventListenerhandler必须同名

作用域:DOM0的handler会在所属元素的作用域内运行,IE的handler会在全局作用域运行,this === window

触发顺序:添加多个事件时,DOM2会按照添加顺序执行,IE会以相反的顺序执行,请谨记

跨浏览器的事件处理程序

var EventUtil = {
  // element是当前元素,可以通过getElementById(id)获取
  // type 是事件类型,一般是click ,也有可能是鼠标、焦点、滚轮事件等等
  // handle 事件处理函数
  addHandler: (element, type, handler) => {
    // 先检测是否存在DOM2级方法,再检测IE的方法,最后是DOM0级方法(一般不会到这)
    if (element.addEventListener) {
      // 第三个参数false表示冒泡阶段
      element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
      element.attachEvent(`on${type}`, handler)
    } else {
      element[`on${type}`] = handler;
    }
  },

  removeHandler: (element, type, handler) => {
    if (element.removeEventListener) {
      // 第三个参数false表示冒泡阶段
      element.removeEventListener(type, handler, false);
    } else if (element.detachEvent) {
      element.detachEvent(`on${type}`, handler)
    } else {
      element[`on${type}`] = null;
    }
  }
}

// 获取元素
var btn = document.getElementById('btn');
// 定义handler
var handler = function(e) {
  console.log('我被点击了');
}
// 监听事件
EventUtil.addHandler(btn, 'click', handler);
// 移除事件监听
// EventUtil.removeHandler(button1, 'click', clickEvent);

三、事件对象

DOM0和DOM2的事件处理程序都会自动传入event对象

IE中的event对象取决于指定的事件处理程序的方法(上面说过)

IE的handler会在全局作用域运行,this === window
所以在IE中会有window.eventevent两种情况

只有在事件处理程序期间,event对象才会存在,一旦事件处理程序执行完成,event对象就会被销毁

event对象里需要关心的几个属性

this、currentTarget、target

这三个属性跟冒泡和捕获有关
target永远是被添加了事件的那个元素,thiscurrentTarget就不一定了(延伸思考:事件处理程序在父节点中的情况)

eventPhase

调用事件处理程序的阶段,有三个值
1:捕获阶段
2:处于目标
3:冒泡阶段

阻止默认preventDefault与传播stopPropagation

preventDefault:比如链接被点击会导航到其href指定的URL,这个就是默认行为

stopPropagation:立即停止事件在DOM层次中的传播,包括捕获和冒泡事件

IE中的对象的对应属性

srcElement => target
returnValue => preventDefaukt()
cancelBubble => stopPropagation()
IE 不支持事件捕获,因而只能取消事件冒泡,但stopPropagation可以同时取消事件捕获和冒泡

四、跨浏览器的事件对象

根据上面对不同类型的事件以及属性区分

var EventUtil = {
  addHandler: (element, type, handler) => {},

  removeHandler: (element, type, handler) => {},
  // 获取event对象
  getEvent: (event) => {
    return event ? event : window.event
  },
  // 获取当前目标
  getTarget: (event) => {
    return event.target ? event.target : event.srcElement
  },
  // 阻止默认行为
  preventDefault: (event) => {
    if (event.preventDefault) {
      event.preventDefault()
    } else {
      event.returnValue = false
    }
  },
  // 停止传播事件
  stopPropagation: (event) => {
    if (event,stopPropagation) {
      event.stopPropagation()
    } else {
      event.cancelBubble = true
    }
  }
}

五、事件委托

这一小节在《高程》P403 我自认不能写的比它更精简更好,有些地方就直接搬过来了

事件委托用来解决事件处理程序过多的问题

页面结构如下

<ul id="myLinks">
  <li id="goSomewhere">Go somewhere</li>
  <li id="doSomething">Do something</li>
  <li id="sayHi">Say hi</li>
</ul>

按照传统的做法,需要像下面这样为它们添加 3 个事 件处理程序。

var item1 = document.getElementById("goSomewhere");
var item2 = document.getElementById("doSomething");
var item3 = document.getElementById("sayHi");
EventUtil.addHandler(item1, "click", function(event){
    location.href = "http://www.wrox.com";
});
EventUtil.addHandler(item2, "click", function(event){
    document.title = "I changed the document's title";
});
EventUtil.addHandler(item3, "click", function(event){
    alert("hi");
});

如果在一个复杂的 Web 应用程序中,对所有可单击的元素都采用这种方式,那么结果就会有数不 清的代码用于添加事件处理程序。此时,可以利用事件委托技术解决这个问题。使用事件委托,只需在 DOM 树中尽量最高的层次上添加一个事件处理程序,如下面的例子所示

var list = document.getElementById("myLinks");
EventUtil.addHandler(list, "click", function(event) {
  event = EventUtil.getEvent(event);
  var target = EventUtil.getTarget(event);
  switch(target.id) {
  case "doSomething":
      document.title = "I changed the document's title";
      break;
  case "goSomewhere":
      location.href = "http://www.wrox.com";
      break;
  case "sayHi": 9 alert("hi");
    break; 
  }
}

子节点的点击事件会冒泡到父节点,并被这个注册事件处理

最适合采用事件委托技术的事件包括 clickmousedownmouseupkeydownkeyupkeypress。 虽然 mouseovermouseout 事件也冒泡,但要适当处理它们并不容易,而且经常需要计算元素的位置。

可以考虑为 document 对象添加一个事件处理程序,用以处理页面上发生的某种特定类型的事件,需要跟踪的事件处理程序越少,移除它们就越容易(移除事件处理程序关乎内存和性能)。
只要是通过 onload 事件处理程序添加的东西,最后都要通过 onunload 事件处理程序将它们移除

在事件处理程序中删除按钮也能阻止事件冒泡。目标元素在文档中是事件冒泡的前提。

参考

DOM 的事件傳遞機制:捕獲與冒泡 | TechBridge 技術共筆部落格
What Is Event Bubbling in JavaScript? Event Propagation Explained