事件


2019/10/27

# 名词

  • 事件 - 编程时运行时环境(runtime)提供给引擎的API,用于对自身发生的事情做出监听(事件监听器,留意事件是否发生)与回应(事件处理器,对事件发生做出回应)
    有三种方式可以为DOM元素注册事件处理函数:
    • EventTarget.addEventListener - EventTarget.addEventListener('click', function() { });
    • HTML 属性(又叫内联模型) - <button onclick="alert('Hello world!')">
    • DOM 元素属性(又叫脚本模型) - myButton.onclick = function(event){ };
  • 事件监听器 - 一般指通过 EventTarget.addEventListener() 注册的函数或对象
  • 事件处理器 - 一般指通过 on-event 属性注册的函数
  • 广义 addEventListener() - DOM 2 Events 规范中引入,可以将指定的监听器注册到 EventTarget 上指定事件类型的事件侦听器列表中,当该对象触发指定的事件时,指定的回调函数就会被依次执行。默认绑定在冒泡阶段,可通过参数修改。
  • on-event 处理器 - DOM 0 规范,指注册事件处理器至指定 DOM元素 上,作为元素属性,在运行时环境产生相应事件时自动触发。绑定在冒泡阶段,不可修改

# addEventListener()

# 示例

element.addEventListener('click', function() { /* do stuff here*/ }, false);
// IE 9 以下
element.attachEvent('onclick', function() { /* do stuff here*/ });

理论上是可以无限次的添加监听器的,唯一的实际的限制是各客户端的内存上限以及相关的性能表现,这在不同的浏览器中会存在区别

# 参数

  1. type - 表示监听事件类型的字符串
  2. listener - 所监听的事件触发时,将会收到一个事件通知对象 Event ,可以是一个函数,也可以是一个带有 handleEvent() 函数属性对对象
    element.addEventListener('click', function (event) { });
    element.addEventListener('click', { handleEvent: function (event) { } });
    
    • 该参数为函数时,this 指向的是当前添加 listener 的 dom 元素,与 currentTarget 参数一致
    • 该参数为带有 handleEvent() 函数属性对象时,this 指向该对象,即 { handleEvent: function (event) { } }
    • 若添加的 listener 为箭头函数,则 this 以箭头函数规则为准
    • 事件通知对象 event 上常用的几个参数:
      • stopPropagation() - 阻止捕获和冒泡阶段中当前事件的进一步传播
      • stopImmediatePropagation() - 阻止事件冒泡并且阻止相同事件的其他侦听器被调用
      • preventDefault() - 阻止事件的默认行为,但不阻止事件继续传播,如点击复选框之后的默认行为是将复选框选中,调用 event.preventDefault() 可阻止该默认行为
      • cancelable - 表明该事件是否可以被取消默认行为,若该事件可以通过 event.preventDefault() 阻止默认行为,则返回 true
      • currentTarget - 当前注册事件的对象的引用,可理解为执行 addEventListener 的元素
      • target - 对事件起源目标的引用,可理解为当前事件发生的实际触点
  3. options(可选) - 指定有关 listener 属性的可选参数对象
    • capture - 若设置为 true,表示 listener 将在该事件捕获阶段传播至该 EventTarget 时触发
    • once - 若设置为 true,表示 listener 在添加之后最多只调用一次,调用完之后会自动移除 listener
    • passive - 若设置为 true,表示 listener 永远不会调用 preventDefault(),若调用了则会忽略并抛出警告
  1. 在旧版本的DOM的规定中,第三个参数是一个布尔值表示是否在捕获阶段调用事件处理程序,而后因为需要增加新的控制条件,所以直接将第三个参数修订为各种属性的对象
  2. 对于直接绑定在事件目标上的 listener ,当前处于“目标阶段”,则会触发该元素(即事件目标)上的所有监听器,而不在乎这个监听器到底在注册时useCapture 参数值是true还是false,并且事件目标上的 listener 将按照绑定的先后顺序执行

# 特性

  • 它对任何 DOM 元素都是有效的,而不仅仅只对 HTML 元素有效
  • 提供了一种更精细的手段控制 listener 的触发阶段,可以选择捕获或者冒泡
  • 可在事件分派时添加事件处理器,如可能在捕获阶段 添加,然后在冒泡阶段被 触发
  • 多个相同的事件处理器会被自动移除,除非 options 的 capture 参数不一致

# 阻止默认行为

浏览器的默认行为,如点击 <a> 的跳转,页面滚动,点击复选框,可通过以下方式阻止

  • event.preventDefault(); - 存在兼容性问题
  • event.returnValue = false; - 存在兼容性问题
  • return false; - 一般写在函数的最后边,不存在兼容性问题

# 页面滚动问题

默认情况下 passive 选项始终是 false ,但这将会使得在执行某些触摸事件时的页面滚动性能的降低,因为对于浏览器来说,在 passive 为 false 的情况下并不知道是否会阻止默认行为,默认行为需要等到捕获、冒泡阶段上添加的 listener 都执行完毕才会执行,而如果添加的 listener 过多或者执行时间过长,势必造成滚动的卡顿!
因此某些浏览器已将文档级节点(Window,Document 和 Document.body)的 touchstart 和 touchmove 事件的 passive 选项的默认值更改为 true,这意味着告诉浏览器我这里处理的业务逻辑并不会阻止页面默认行为(永远不会调用 preventDefault()),此时 listener 并不会阻塞默认行为

因此我们在某些场景下需要阻止页面的滚动,并且页面保持在当前滚动位置,此时我们可以禁用掉滚轮事件(wheel),而滚轮事件的 passive 在某些浏览器下默认是 true 的,因此我们需要显示的将 passive 设置成 false ,并在 listener 中通过 preventDefault() 禁用浏览器默认事件,进而完成我们想要的效果

# on-event 处理器

on-event 处理器是由 DOM元素 提供的一组属性,存在两种方式:内联、脚本:

<!-- 内联模型 -->
<button onclick="return handleClick(event);">
// 脚本模型
document.getElementById("mybutton").onclick = function(event) { ... }

每个对象对于给定的事件只能有一个事件处理器,后续添加将覆盖之前的
添加的事件处理器类似于普通对象的属性,可以通过名称直接获取:

var div = document.getElementById("a");
div.onclick = function() { alert('new') };
div.onclick.toString(); // function () { alert('new') }
// 注意:这里处理器内的 this 指向,与 addEventListener 中的 listener 类似,普通情况下为当前注册改事件处理器的 DOM 元素,箭头函数则遵循箭头函数规则

某些 dom元素 上的某些处理器是通过继承 Window 对象上的事件处理器而来的,类似于 onblur, onerror, onfocus, onload, onscroll

# EventTarget.addEventListener() 与 on-event 的比较

  • EventTarget.addEventListener() 由 DOM Event 2 规范提出,存在一定的浏览器兼容性限制,例如早期的 IE 只支持 attachEvent 事件,而 on-event 由 DOM Event 0 规范提出,兼容所有浏览器
  • EventTarget.addEventListener() 提供更精细的事件流控制,而 on-event 只能添加事件绑定
  • EventTarget.addEventListener() 提供同一事件的多个 listener 绑定,适合于更复杂的业务场景,而 on-event 只能绑定一个事件处理器,后续添加的将覆盖之前的

# 捕获阶段 与 冒泡阶段

  • 捕获阶段(capturing phase) - 由顶层文档级节点开始,逐层向下传递事件,最终找到目标元素
  • 目标阶段(target phase) - 找到目标元素,执行添加的 事件监听器 与 事件处理器,事件处理器先执行
  • 冒泡阶段(bubbling phase) - 从目标元素开始,逐层向上传递事件,最终找到顶层文档级节点

一图说明所有问题:

捕获阶段 与 冒泡阶段

# 事件委托

事件委托 - 将事件监听器统一绑定在父级元素上,通过 target 分流当前真正的触点,好处是在存在多个子节点都需要绑定事件时,只需要将事件监听委托给父级元素,从而减少了 listener 的绑定,也就减小了性能开销

手写一下面试官最喜欢考的事件委托,只说明原理,之际业务场景肯定要复杂的多

<ul class="ul">
    <li class="li">111</li>
    <li class="li">222</li>
    <li class="li">333</li>
    <li class="li">444</li>
</ul>
document.getElementsByClassName('ul')[0].addEventListener('click', event => {
	var e = event || window.event;
	var target = e.target || e.srcElement;
	if (target.nodeName.toLowerCase() == 'li') {
        console.log(target.innerHTML);
    }
})
Last Updated: 12/2/2019, 9:23:57 AM