Erik's Blog

《JavaScript 高级程序设计》笔记(5) —— 事件机制

(更新于 )JavaScript#JavaScript

事件流、使用事件处理程序、不同的事件类型

通过 观察员模式 使行为和表现松耦合

第 13 章 事件

事件流

事件流描述的是从页面中接收事件的顺序。

事件冒泡 : IE 提出。事件开始时有最具体的元素(层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。IE9+ 等冒泡到 window 对象。

事件捕获 : Netscape 提出。不太具体的节点更早接收到事件。document 最先接收。建议使用 事件冒泡。

DOM 事件流 :

  • DOM2 级事件 规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。document
  • 规范要求事件捕获阶段不涉及事件目标,但浏览器都会在捕获阶段触发对象上的事件。

事件处理程序

  • 事件 : 用户或浏览器自身执行的某种动作(click load mouseover 等)
  • 事件处理程序/事件侦听器 : 响应某个事件的函数,以 on 开头(onclick onload …)

HTML 事件处理程序

  • 某个元素支持的每种事件,都可以使用一个与响应事件处理程序同名的 HTML 特性来指定(onclick=“alert(‘Hi’)”)

  • 事件处理程序中的代码在执行时有权访问全局作用域中的任何代码

  • 通过 HTML 事件处理程序指定的事件处理程序会创建一个封装着元素属性值的函数

  • 函数内通过 event 变量访问事件对象,this 值等于目标元素

  • 函数使用 with 扩展了作用域,可以直接访问自己的属性 : with(document){with(this){…}}

  • 如果当前元素是一个表单输入元素,作用域还会包含访问表单元素的入口 : with(document){with(this.form){with(this){…}}}

  • 缺点1 : 存在时差,触发事件时事件处理程序可能不具备执行条件(try-catch 防止错误浮出水面)

  • 缺点2 : 作用域链在不同浏览器中有不同的结果,访问非限定对象时出错

  • 缺点3 : HTML 与 JavaScript 紧密耦合

DOM0级事件处理

  • 指定事件处理程序 : 将一个函数赋值给一个事件处理程序属性 btn.onclick = function(){alert(1);};
  • this 引用当前元素
  • 在事件冒泡阶段被处理
  • 删除事件处理程序 : 赋 null btn.onclick = null;

DOM2 级事件处理程序

  • addEventListener(“事件名”, 处理函数, true/false)

    • 事件名 如 click
    • 处理函数传入 event 事件对象
    • true 捕获阶段调用事件处理程序
    • false 冒泡阶段调用事件处理程序
    • 在其依附的元素的作用域中运行
    • 事件按添加顺序触发
  • removeEventListener(“事件名”, 处理函数, true/false)

    • 传入相同的参数移除(通过匿名函数添加的无法移除)

大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段以最大限度的兼容各种浏览器。

IE事件处理程序 : IE Opera 支持

  • attchEvent(“事件处理程序属性”, handler) 添加事件

    • 事件处理程序属性 如 onclick
    • 处理函数传入 event 事件对象
    • 函数在全局作用域中运行 this==window
    • 按添加顺序相反的顺序被触发
  • detachEvent(“事件处理程序属性”, handler) 移除事件

跨浏览器的事件处理程序 能力检测

var EventUtil = {
	addHandler : function(element, type, handler) {
		if (element.addEventListener) {
			element.addEventListener(type, handler, false);
		} else if (element.attchEvent) {
			element.attchEvent("on" + type, handler);
		} else {
			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;
		}
	}
}

事件对象

在触发 DOM 上的某个事件时,会产生一个事件对象 event,包含着所有与事件有关的信息。

DOM 中的事件对象

属性 / 方法 类型 读写 说明
eventPhase Integer 只读 调用事件处理程序的阶段。1 : 捕获,2 : 处于目标,3 : 冒泡
target Element 只读 事件的目标
type String 只读 倍触发事件的类型
currentTarget Element 只读 其事件处理程序当前正在处理事件的那个元素
defaultPrevented Boolean 只读 true 表示已经调用了 preventDefault() (DOM3)
detail Integer 只读 与事件相关的细节信息
preventDefault() Function 只读 取消事件的默认行为。cancelable==true 时可以使用这个方法
cancelable Boolean 只读 表明是否可以取消事件的默认行为
stopImmediatePropagation() Function 只读 取消事件的进一步捕获或冒泡,同时阻止任何事件处理程序被调用 (DOM3)
stopPropagation() Function 只读 取消事件的进一步捕获或冒泡。如果 bubbles==true 时可以使用这个方法
bubbles Boolean 只读 表明事件是否冒泡
trusted Boolean 只读 true 事件是由浏览器生成的; false 事件是由开发人员通过 JavaScript 创建的 (DOM3)
view AbstractView 只读 与事件关联的抽象视图。等同于事件发生的 window 对象
  • 事件处理程序内部,this==currentTarget,target 只包含事件的事件目标。

  • 直接将事件处理程序指定给目标元素(onclick=function),this==currentTarget==target。

  • 事件处理程序位于父节点,this==currentTarget!=target。

  • 通过一个函数处理多个事件,使用 type 属性。switch(event.type)

  • event.preventDefault() : 阻止特定事件的默认行为。

  • event.stopPropagation() : 立即停止事件在 DOM 层次中的传播。

IE 中的事件对象

  • DOM0 级方法添加事件处理程序时,event 对象作为 window 对象的一个属性存在。event.srcElement==this
  • attachEvent() 方式添加时,event 对象作为参数传入函数并可以通过 window.event 访问。event.srcElement!=this
属性/方法 类型 读写 说明
cancelBubble Boolean 读写 默认 false,设为 true 可以取消事件冒泡
returnValue Boolean 读写 默认 true,设为 false 可以取消事件的默认行为
srcElement Element 只读 事件的目标,同 DOM 中的 target
type String 只读 被触发事件的类型

跨浏览器的事件对象

var EventUtil = {
  addHandler: function (element, type, handler) {
    if (element.addEventListener) {
      element.addEventListener(type, handler, false);
    } else if (element.attchEvent) {
      element.attchEvent("on" + type, handler);
    } else {
      element["on" + type] = handler;
    }
  },

  getEvent: function (event) {
    return event ? event : window.event;
  },

  geetTarget: function (event) {
    return event.target || event.srcElement;
  },

  preventDefault: function (event) {
    if (event.preventDefault) {
      event.preventDefault();
    } else {
      event.returnValue = false;
    }
  },

  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;
    }
  },

  stopPropagation: function (event) {
    if (event.stopPropagation) {
      event.stopPropagation();
    } else {
      event.cancelBubble = true;
    }
  }
};

事件类型

DOM3 级事件规定的几类事件 :

UI 事件 : 当用户与页面上的元素交互时触发

  • load :

    • 页面完全加载后在 window 上触发
    • 所有框架都加载完时在框架集合上触发
    • 图像加载完成后在 img 上触发 (图像在设置了 src 属性时开始下载,在指定 src 前先指定事件)
    • script 元素以非标准的方式支持 load 事件 IE8+ (设置 src 并添加到文档后才会开始下载 JavaScript 文件,顺序不重要)
    • IE Opera 支持 link 上的 load
  • unload :

    • 页面完全卸载后在 window 上面触发
    • 当所有框架都写在后在框架集合上触发
    • 嵌入的内容卸载完毕后在 <object> 元素上触发
    • 一般用于用户从一个页面切换到另一个页面时清除引用,以避免内存泄漏
  • resize :

    • 当窗口或框架的大小变化时在 window 或框架上面触发
    • 不应再这个方法中加入大量代码,以防浏览器变慢
  • scroll :

    • 当用户滚动带滚动条的元素中的内容时,在该元素上触发
    • 不应再这个方法中加入大量代码,以防浏览器变慢
    • 需考虑浏览器兼容情况
  • abort : 在用户停止下载过程时,如果嵌入的内容没有加载完,在 object 元素上面触发

  • error : 当发生 JavaScript 错误时在 window 上面触发,当无法加载图像时在 img 元素上面触发,当无法加载嵌入内容时在 object 元素上面触发,当有一个或多个框架无法加载时在框架集合上面触发

  • select : 当用户选择文本框(input/textarea)中的字符时触发

焦点事件 : 当元素获得或失去焦点时触发

利用焦点事件、document.hasFocus()、document.activeElement 配合知晓用户在页面上的行踪。

  • blur : 元素失去焦点时触发。不会冒泡
  • focus : 元素获得焦点时触发。不会冒泡
  • focusin : 元素获得焦点时触发。冒泡
  • focusout : 元素失去焦点时触发。冒泡
  • DOMFocusIn : 元素获得焦点时触发,Opera
  • DOMFocusOut : 元素失去焦点时触发,Opera

focusout > focusin > blur > DOMFocusOut > focus > DOMFocusIn

鼠标事件 : 当用户通过鼠标在页面上执行操作时触发

  • clink : 用户单击主鼠标键或按下键盘回车键时触发

  • dbclick : 双击主鼠标键

  • mousedown : 按下任意鼠标按钮,不能通过键盘触发

  • mouseenter : 鼠标从元素外部首次移动到元素范围之内时触发。不冒泡,移动到后代元素不会触发

  • mouseleave : 鼠标从元素内部移动到元素范围之外时触发。不冒泡,移动到后代元素不会触发

  • mousemove : 当鼠标指针在元素内部移动时重复地触发,不能通过键盘触发

  • mouseout : 鼠标有一个元素上移入另一个元素时触发(可以是子元素),不能通过键盘触发

  • mouseover : 鼠标位于一个元素外部,首次移入另一个元素边界之内时触发,不能通过键盘触发

  • mouseup : 用户释放鼠标按键时触发,不能通过键盘触发

  • 取消鼠标事件的默认行为会影响其他事件 click=mousedown+mouseup dbclick=2click

mousedown > mouseup > click > mousedown > mouseup > click > dbclick IE8- 有 bug

  • 客户区坐标位置 : clientX clientY 不包括页面滚动距离

  • 页面坐标位置 : pageX pageY IE9+, IE8- 使用 document.body(混杂模式) 或 document.documentElement(标准模式) 的 scrollLeft/scrollTop 属性替代

  • 屏幕坐标位置 : screenX screenY

  • 修改键 : DOM 为 event 规定了 shiftKey ctrlKey altKey metaKey 四个属性 IE9+

  • 相关元素 : event.relatedTarget ,mouseover 和 mouseout 事件主目标外的另一个元素。IE9+

var EventUtil = {
	...

  getRelatedTarget: function (event) {
    if (event.relatedTarget) {
      return event.relatedTarget;
    } else if (event.toElement) {
      return event.toElement;
    } else if (event.fromElement) {
      return event.fromElement;
    }
  },
  ...
  getButton: function (event) {
    if (document.implementation.hasFeature("MouseEvents", "2.0")) {
      return event.button;
    } else {                                               // IE8-
      switch (event.button) {
        case 0:
        case 1:
        case 3:
        case 5:
        case 7:
          return 0;
        case 2:
        case 6:
          return 2;
        case 4:
          return 1;
      }
    }
  }

  getWheelDelta: function (event) {
    if (event.wheelDelta) {
      return (client.engine.opera && client.engine.opera < 9.5 ? -event.wheelDelta : event.wheelDelta);
    } else {
      return -event.detail * 40;
    }
  }

  getCharCode: function (event) {
    if (typeof event.charCode == "number") {
      return event.charCode;
    } else {
      return event.keyCode;
    }
  }
}
  • 鼠标按钮 : event.button , 0:主 1:中 2:次 IE9+

  • event.detail : 点击次数,mousedown 到 mouseup 之间有移动则置 0

  • IE 独有的属性 …

滚轮事件 : 当使用鼠标滚轮(或类似设备)时触发 mousewheel

  • event.wheelDelta : 120*n 一般只检测符号 向前滚动为正 向后滚动为负

  • 触摸设备 :

    • 不支持 dbclick 事件
    • 轻击可单击元素(a/onclick)会触发 mousemove 事件
      • 如果导致内容变化,不会再有其他事件发生
      • 如果没有变化,触发 mousedown mouseup click 事件
    • 轻击不可单击元素,不会触发事件
    • mousemove 事件也会触发 mouseover 和 mouseout 事件
    • 双指滚动触发 mousewheel 和 scroll 事件
  • 无障碍性问题

键盘事件、文本事件 : 当用户通过键盘在页面上执行操作时触发 、当在文档中输入文本时触发

  • keydown : 按下 任意键 触发,按住不放重复触发

  • keypress : 按下 字符键/Esc 触发,按住不放重复触发

  • keyup : 释放键盘上的键时触发

  • textInput : 补充 keypress 在文板块发生变化前触发

  • event.shiftKey …

keydown > keypress > 文本框变化 > keyup

  • 键码 : event.keyCode、IE8-/Opera 使用 charCode ASCII 小写 p380

  • DOM3 级变化 :

    • key(取代 keyCode)存储文本字符;
    • char 同 key ,但非字符为 null;
    • location 0:默认 1:左侧 2:右侧
  • textInput 事件 : 只有可编辑区域输入实际字符时才能触发

    • event.data 输入字符 IE9+
    • event.inputMethod 输入方式 只有IE p383

合成事件 : 当为 IME 输入字符时触发

变动事件 : 当底层 DOM 结构发生变化时触发 IE9+

  • DOMSubtreeModified : DOM 结构发生任何变化时触发,其它事件触发后不会触发

  • DOMNodeInserted : 节点被插入时触发

  • DOMNodeInsertedIntoDocument : 节点被直接插入文档或通过子树间接插入文档之后触发。DOMNodeInserted 之后触发

  • DOMNodeRemoved : 节点从其父节点移除时触发

  • DOMNodeRemovedFromDocument : 节点直接从文档中或通过子树间接从文档中移除之前触发。DOMNodeRemoved 之后触发

  • DOMAttrModified : 特性被修改之后触发

  • DOMCharacterDataModified : 在文本节点的值发生变化时触发

  • 删除节点 removeChild()/replaceChild()

    1. 触发 DOMNodeRemoved ,event.target=被删节点 event.relatedNode=目标父节点,冒泡
    2. 包含的子节点及被删节点触发 DOMNodeRemovedFromDocument ,不冒泡
    3. 触发 DOMSubtreeModified ,目标是被移除节点的父节点
  • 插入节点 appendChild()/replaceChild()/insertBefore()

    1. 插入后触发 DOMNodeInserted, 目标是被插入的节点; event.telatedNode=目标父节点, 冒泡
    2. 新插入的节点上触发 DOMNodeInsertedIntoDocument, 不冒泡
    3. 触发 DOMSubtreeModified ,目标是插入节点的父节点

变动名称事件 : 当元素或属性名变动时触发,已被抛弃,无浏览器实现

HTML5 事件 (部分)

  • contextmenu 事件 : 冒泡, 可在 document 上处理

    • 目标 : 发生操作的元素
    • 取消默认行为创建自定义菜单
  • beforeunload 事件 : window 上触发, 通过它实现阻止卸载并继续使用原有页面,通过设置 event.returnValue (IE/Firefox) 并返回字符串(Safari/Chorme) 设置要显示给用户的信息

  • DOMContentLoad 事件 : 形成完整的 DOM 树后触发,不理会图像、js、css。在 document 触发,冒泡到 window 。 IE9+

  • readystatechange 事件 : IE/Firefox/Opera …

  • pageshow/pagehide 事件 : 页面显示/隐藏到往返缓存(bfcache)时触发 IE10+

    • 处理程序必须加到 window ;
    • event.persisted 页面保存在 bfcache 中为 true, 否则为 false。
    • 指定了 onunload 事件处理程序的页面不会存入 bfcache
  • hashchange 事件 : URL 参数列表变化时触发 IE8+

    • 处理程序必须加到 window ;
    • event.oldURL event.newURL IE不支持

设备事件

  • orientationchange 事件 : window.orientation : 0/肖像模式 90/左旋 -90/右旋 (移动 Safari)
  • MozOrientation 事件
  • diviceorientation 事件 : event.alpha/beta/gamma/absolute/compassCalibrated iOS
  • devicemotion 事件 : event.acceleration/accelerationIncludeingGravity/interval/rotationRate

触摸与手势

内存和性能

JavaScript 中添加到页面上的事件处理程序数量关系到页面的整体运行性能

事件委托 : 利用事件冒泡,在 DOM 树中尽量最高的层次上添加一个事件处理程序,通过检测 target 进行不同处理

移除事件处理程序 : 在不需要时移除事件处理程序

  • 从文档中移除带有事件处理程序的元素时(removeChild()/replaceChild()/innerHTML), 原有的事件处理程序可能无法回收, 需手工移除
  • 卸载页面时也可能导致 空事件处理程序 , 最好在页面卸载之前通过 onunload 事件处理程序移除所有事件处理程序

模拟事件

可以使用 JavaScript 在任意时刻来触发特定的事件 IE9+

DOM 中的事件模拟

  • document.createEvent(“EventType”) : 创建 Event

    • EventType : DOM2 复数、DOM3 单数
      • UIEvents/UIEvent : 一般化的 UI 事件。鼠标、键盘事件都继承自 UI 事件
      • MouseEvents/MouseEvent : 一般化鼠标事件。
      • MutationEvents/MutationEvent : 一般化的 DOM 变动事件
      • HTMLEvents : 一般化的 HTML 事件。无对应 DOM3 级事件
  • dispatchEvent(event) : 触发事件

  • 模拟鼠标事件 : createEvent(“MouseEvents”).initMouseEvent(type, bubbles, cancelable, view, …)

  • 模拟键盘事件 : DOM3 createEvent(“KeyboardEvent”).initKeyEvent(type, …)

  • 自定义 DOM 事件 : createEvent(“CustomEvent”).initCustomEvent(type, bubbles, cancelable, detail)

IE 中的事件模拟