JS 中事件流和事件处理程序(详细整理)

JS 中事件流和事件处理程序

  JavaScript 与 HTML 的交互是通过事件实现的,事件代表文档或浏览器窗口中某个有意义的时刻(用户或浏览器执行了某种动作)。可以使用仅在事件发生时执行的监听器(也叫处理程序)来订阅事件。这个模型叫 “观察者模式”,其能够做到页面行为(在 JavaScript 中定义)与页面展示(在 HTML 和 CSS 中定义) 的分离。

一、事件流

  当你点击一个按钮时,实际上不光点击了这个按钮,还点击了它的容器以及整个页面。这个点击产生了不止一个事件。而是一个事件流。

  事件流描述的就是从页面中接收事件的顺序。而早期的 IE 和 Netscape 提出了完全相反的事件流概念,IE 事件流是事件冒泡,而 Netscape 的事件流就是事件捕获。

1.1 事件冒泡

  IE 事件流被称为事件冒泡,事件被定义为从最具体的元素开始触发,然后向上传播至没有那么具体的元素 (文档)。比如有如下 HTML 页面:

<!DOCTYPE html>
<html>
    <head>
      <title>Event Bubbling Example</title>
    </head>
    <body>
      <div id="myDiv">Click Me</div>
    </body>
</html>

  在点击页面中的 <div> 元素后,click 事件会以如下顺序发生:

  <div> == > <body> == > <html> ==> document

事件冒泡

  所有现代浏览器都支持事件冒泡,只是在实现方式上会有一些变化。 IE5.5 及早期版本会跳过 <html> 元素(从 <body> 直接到document)。现代浏览器中的事件会一直冒泡到 window 对象。

1.2 事件捕获

  Netscape 团队提出了名为事件捕获的事件流。事件捕获的意思是最不具体的节点最先收到事件,而最具体的节点最后收到事件。如果前面的例子使用事件捕获,则点击 <div>元素会以下列顺序触发click事件:

  document ==> <thml> == > <body> == > <div>

事件捕获

  事件捕获是 Netscape 唯一的事件流模型,但事件捕获在所有现代浏览器上也都支持。实际上,所有浏览器都是从 window 对象开始捕获事件,而 DOM2 Events 规范规定的是从 document 开始。实际当中几乎不会使用事件捕获。建议使用事件冒泡,特殊情况下可以使用事件捕获。

1.3 DOM 事件流

  DOM2 Events 规范规定事件流分为3个阶段:① 事件捕获到达目标事件冒泡。事件捕获最先发生,为提前拦截事件提供了可能。然后,实际的目标元素接收到事件。最后一个阶段是事件冒泡,最迟要在这个阶段响应事件。仍以前面那个简单的 HTML 为例,点击 <div> 元素会以下图的顺序触发事件。

DOM 事件流

  在 DOM 事件流中,实际的目标( <div> 元素)在捕获阶段不会接收到事件。

  • 事件捕获阶段: 从 document 到 <html> 再到 <body> 就结束了。
  • 达到目标阶段: 这个阶段会在 <div>元素上触发事件。
  • 事件冒泡阶段: 通常在事件处理时被认为是冒泡阶段的一部分。然后,冒泡阶段开始, 事件反向传播至文档。

  大多数支持 DOM 事件流的浏览器实现了一个小小的拓展。虽然 DOM2 Events 规范明确捕获阶段不命中事件目标,但现代浏览器都会在捕获阶段在事件目标上触发事件。最终结果是在事件目标上有两个机会来处理事件

  所有现代浏览器都支持 DOM 事件流。

二、事件处理程序

  事件是用户或浏览器执行的某种动作。比如,单击(click)、加载(load)、鼠标悬停(mouseover)。为响应事件而调用的函数被称为事件处理程序(或事件监听器)。事件处理程序的名字以 “on” 开头, 因此 click 事件的处理程序叫作 onclick ,而 load 事件的处理程序叫作onload。这里重点提醒(事件名与事件处理程序名的区别(click ==> onclick))。有很多方式可以指定事件处理程序。

2.1 HTML 事件处理程序

  一个元素支持的事件可以使用事件处理程序的名字作为 HTML 属性,以能够执行的 JS 代码作为值的形式来指定。例如:

<input type="button" value="Click Me" onclick="console.log('Clicked')"/>

注意,因为属性的值是 JS 代码,所以不能在未经转义的情况下使用 HTML 语法字符,比如和号(&)、双引号(")、小于号(<)和大于号(>)。为了避免使用 HTML 实体,可以使用单引号代替双引号。如果确实需要使用双引号,则可以这样:

<input type="button" value="Click Me"onclick="console.log(&quot;Clicked&quot;)"/>

在 HTML 中定义的事件处理程序可以包含精确的动作指令,也可以调用在页面其他地方定义的脚本,比如:

<script>
  function showMessage() {
    console.log("Hello world!");
  }
</script>
<input type="button" value="Click Me" onclick="showMessage()"/>

单击按钮会调用showMessage()函数。调用的函数也可以在外部文件中定义。 作为事件处理程序执行的代码可以访问全局作用域中的一切。

以这种方式指定的事件处理程序有一些特殊的地方。

  • 1.会创建一个函数来封装属性的值。这个函数有一个特殊的局部变量 event ,其中保存的就是 event 对象(后面内容会讨论):
<input type="button" value="Click Me" onclick="console.log(event.type)">
<!-- 输出"click" -->
  • 2.在这个函数中,this 值相当于事件的目标元素:
<input type="button" value="Click Me" onclick="console.log(this.value)">
<!-- 输出"Click Me" -->
  • 3.在这个动态创建的包装函数中,document 和元素自身的成员都可以被当成局部变量来访问。这是通过使用 with 实现的(如下第一段代码)。意味着事件处理程序可以更方便地访问自己的属性。(如下第二段代码)
function() {
  with(document) {
    with(this) { 
        // 属性值
    } 
  }
}
<input type="button" value="Click Me" onclick="console.log(value)">
<!-- 输出"Click Me" -->

如果这个元素是一个表单输入框,则作用域链中还会包含表单元素,事件处理程序对应的函数等价于如下第一段代码。

function() {
  with(document) {
    with(this.form) {
      with(this) {
		// 属性值 
      }
	} 
  }
}

经过这样的扩展,事件处理程序的代码就可以不必引用表单元素,而直接访问同一表单中的其他成员了。下面的例子就展示了这种成员访问模式:

<form method="post">
	<input type="text" name="username" value="">
	<input type="button" value="Echo Username" onclick="console.log(username.value)">
</form>

在 HTML 中指定事件处理程序有一些问题。

  • 1.时机问题:html 元素已经显示,用户在与其交互,但事件处理程序还没定义无法执行。此时事件触发会跑出错误。

    处理方法:大多数 HTML 事件处理程序会封装在 try/catch 块中,以便在这种情况下无视报错如下代码,如果在 showMessage() 函数被定义之前点击了按钮,就不会发生 JavaScript 错误了,因为错误在浏览器收到之前已经被拦截了。

<input type="button" value="Click Me" onclick="try{showMessage();}catch(ex) {}">
  • 2.对事件处理程序作用域链的扩展在不同浏览器中可能导致不同的结果。不同 JavaScript 引擎中标识符解析的规则存在差异,因此访问无限定的对象成员可能导致错误。
  • 3.HTML 与 JavaScript 强耦合。如果需要修改事件处理程序,则必须在两个地方,即 HTML 和 JavaScript 中,修改代码。所以很多开发者不使用 HTML 事件处理程序。

2.2 DOM0 事件处理程序

  DOM0 事件处理程序即在 JS 中把一个函数赋值给 DOM元素的一个事件处理程序属性。DOM 中每个元素(包括 window 和 document )都有通常小写的事件处理程序属性,比如onclick。只要把这个属性赋值为一个函数即可添加事件处理程序,要使用 JS 指定事件处理程序,先取得要操作对象的引用。所有现代浏览器仍然都支持此方法。

let btn = document.getElementById("myBtn");
btn.onclick = function() {
  console.log("Clicked");
};

【注意】 前面的代码在运行之后才会给事件处理程序赋值。如果代码出现在按钮之后,则可能出现用户点击按钮没有反应的情况。

  使用 DOM0 方式为事件处理程序赋值时,所赋函数被视为元素的方法。因此,事件处理程序会在元素的作用域中运行,即 this 等于元素

let btn = document.getElementById("myBtn");
btn.onclick = function() {
  console.log(this.id);  // "myBtn"
};

  DOM0 方式添加事件处理程序是注册在事件流的冒泡阶段的。

  要移除 DOM0 方式添加的事件处理程序,将事件处理程序属性的值设置为 null 即可。事件处理程序是在HTML中指定的,也可以在 JS 中将相应属性设置为 null 来移除。

btn.onclick = null; // 移除事件处理程序

2.3 DOM2 事件处理程序

  DOM2 Events 为事件处理程序的赋值和移除定义了两个方法:addEventListener()removeEventListener()。所有 DOM 节点都有这两个方法,它们接收 3 个参数:事件名(注意不是事件处理函数名,不要 on)事件处理函数和一个布尔值true 表示在捕获阶段调用事件处理程序,false(默认值)表示在冒泡阶段调用事件处理程序。

let btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
  console.log(this.id);
}, false);

  这个事件处理程序同样在被附加到的元素的作用域中运行,this 指向元素。不同的是 DOM2 方式可以为同一个事件添加多个事件处理程序。多个事件处理程序以添加顺序来触发,因此下面的代码会先打印元素 ID,然后显示消息 “Hello world!”。

let btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
  console.log(this.id);
}, false);
btn.addEventListener("click", () => {
  console.log("Hello world!");
}, false);

  通过 addEventListener() 添加的事件处理程序只能使用 removeEventListener() 并传入与添加时同样的参数来移除。这意味着使用 addEventListener() 添加的匿名函数无法移除:

let btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {		// 匿名事件处理函数
  console.log(this.id);
 }, false);
btn.removeEventListener("click", function() {		// 没有效果
  console.log(this.id);
}, false);

let handler = function() {
  console.log(this.id);
};
btn.addEventListener("click", handler, false);
btn.removeEventListener("click", handler, false);	// 有效果!

  大多数情况下,事件处理程序会被添加到事件流的冒泡阶段,主要原因是跨浏览器兼容性好。把事件处理程序注册到捕获阶段通常用于在事件到达其指定目标之前拦截事件。如果不需要拦截,则不要使用事件捕获。

2.4 IE 事件处理程序

  IE 实现了与 DOM2 类似的方法,即 attachEvent() 附加事件和 detachEvent() 剥离事件。这两个方法接收两个参数:事件处理程序的名字(注意不是事件名)和事件处理函数。使用 attachEvent() 添加的事件处理程序会添加到冒泡阶段。

let btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function() {
  console.log("Clicked");
});

IE 事件处理程序注意以下几点:

  • 使用 attachEvent() 时,事件处理程序是在全局作用域中运行的,因此 this 等于 window。这个差异对编写跨浏览器代码是非常重要的。
  • 使用 attachEvent() 方法也可以给一个元素添加多个事件处理程序。事件处理程序会以添加它们的顺序反向触发
  • 使用 attachEvent() 添加的事件处理程序将使用 detachEvent() 来移除, 只要提供相同的参数。匿名函数也无法移除。

2.5 跨浏览器事件处理程序

  为了以跨浏览器兼容的方式处理事件,需要编写跨浏览器事件处理代码,主要依赖能力检测。要确保事件处理代码具有最大兼容性,只需要让代码在冒泡阶段运行即可。

  首先创建一个包含添加事件处理程序方法 addHandler() 和移除事件处理程序方法 removeHandler()的对象 EventUtil。

  addHandler() 方法的任务是根据需要分别使用 DOM0 方式、DOM2 方式或 IE 方式来添加事件处理程序。接收3个参数:目标元素、事件名和事件处理函数。

  removeHandler() 方法的任务就是移处之前添加的事件处理程序,不管是通过何种方式添加的。接收与添加时相同的参数。

var EventUtil = {
  addHandler: function(element, type, handler) {
    if (element.addEventListener) {		// DOM2 方式
      element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {	// IE 方式
      element.attachEvent("on" + type, handler);
    } else {							// DOM0 方式
      element["on" + type] = handler;
} },
  removeHandler: function(element, type, handler) {
    if (element.removeEventListener) {
      element.removeEventListener(type, handler, false);
    } else if (element.detachEvent) {
      element.detachEvent("on" + type, handler);
    } else {
      element["on" + type] = null;
    }
} };

  两个方法都是首先检测传入元素上是否存在 DOM2 方式。如果有就使用该方式,传入事件类型和事件处理函数,以及表示冒泡阶段的第三个参数 false。否则,如果存在 IE方式,则使用该方式。注意这时候必须在事件类型前加上 “on”,才能保证在 IE8 及更早版本中有效。最后是使用 DOM0 方式(在现代浏览器中不会到这一步)。注意使用 DOM0 方式时使用了中括号计算属性名,并将事件处理程序或 null 赋给了这个属性。

let btn = document.getElementById("myBtn")
let handler = function() {
  console.log("Clicked");
};
EventUtil.addHandler(btn, "click", handler);
EventUtil.removeHandler(btn, "click", handler);

  这里的 addHandler() 和 removeHandler() 方法并没有解决所有跨浏览器一致性问题,比如 IE 的作用域问题、多个事件处理程序执行顺序问题等。另外也要注意,DOM0 只支持给一个事件添加一个处理程序。好在 DOM0 浏览器已经很少有人使用了,所以影响应该不大。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ItDaChuang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值