69
一步,一步前進の一步

图片描述

事件是文档或者浏览器窗口中发生的一些交互瞬间。JS注册事件处理程序来预订事件,当事件发生的瞬间来执行相应的代码,进而实现 JS 和 HTML(即文档或者浏览器窗口) 的交互。

事件流

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

用手指戳一下屏幕上的同心圆的中心,先点到的是最外围的大圆,还是最核心的小圆呢?这个就是事件流要处理的本质问题。早起的 IE 和 Netscape 对此有不同的观点,IE认为先点到的是最小的圆,然后在一层层的传递到最外面的大圆(事件冒泡),Netscape 正好相反,最先碰到的是最外围的大圆,然后在一层层的追踪到最精准的小圆(事件捕获)。

事件冒泡:事件开始由最具体的元素接收,然后逐级向上传播到较为不具体的节点。

事件捕获:事件是从不太具体的节点开始产生接收,而最具体的节点应该是最后接收事件的。

事件流规范出来说,事件传递有三个阶段: 捕获阶段、目标阶段、冒泡阶段。
图片描述

事件处理程序

浏览器的事件处理大概就是注册、监听。程序开始就对未来会发生的某些事情,做出预期,对预期做出正确的反应。事件处理程序就是被注册正确反应监听这个操作由浏览器自己完成。
浏览器提供了三种方法,为事件绑定监听函数。

html 属性方式

<div onclick="doSomething()">

需要注意的是:此处的事件处理程序是需要带小括号的,大概的过程是当 div 接收到事件时,会将onclick后面的代码原封不动的传入JavaScript引擎执行,不加小括号就不会触发处理程序。

此方式会让 js 的代码和 html 代码杂糅在一起,不易代码的变更和维护,因此不推荐使用。

Dom 0级事件注册

div.onclick = function (event) {
  console.log('触发事件');
};

以元素节点对象的事件属性的方式进行注册,与第一种方式类似。
该事件只会在冒泡阶段触发。

Dom 2级事件注册

target.addEventListener(type, listener[, useCapture])
type事件名称,大小写敏感;listener处理函数;useCapture是否在捕获阶段触发。
推荐使用该方式进行事件的注册,可以对同一节点注册多个事件处理函数。当前冒泡流是被大多浏览器支持,因此useCapture大多赋为false。

document.addEventListener('click', hello, false);
document.addEventListener('click', hello2, false);

IE 事件处理程序

早期的 IE 浏览器只支持冒泡流,有自己的事件注册和移除的方法:attachEvent()detachEvent()

btn.attachEvent('onclick', function () {
    alert('ie browser');
});

需要注意的是处理函数的 this 是指向 window 的,而不像前两种事件处理函数的 this 会指向事件所在的 dom 节点对象。

跨浏览器事件处理程序

红宝书方案代码如下:

var EventUtil = {
    addHandler: function(elm, type, handler) {
        if (elm.addEventListener) {
            elm.addEventListener(type, handler, false);
        } else if (elm.attachEvent) {
            elm.attachEvent('on' + type, handler);
        } else {
            elm['on' + type] = handler;
        }
    }
};

该方法是比较好的,但是处理函数执行时的 this 指向还是有一点点问题,attachEvent方式还是指向 window 的,如果想更加完善,请参考 js 忍者秘籍上面的方案。

在处理事件时,我们需要考虑一些性能的问题,有必要限制事件处理函数的数量,适当的时候将已有的事件处理程序移除掉或者采用事件委托机制减少注册的个数。下面我们简单谈谈如何移除事件处理程序。

var EventUtil = {
    removeHandler: function (elm, type, handler) {
        if (elm.removeEventListener) {
            elm.removeEventListener(type, handler, false);
        } else if (elm.detachEvent) {
            elm.detachEvent('on' + type, handle);
        } else {
            elm['on' + type] = handler;
        }
    }
}

事件的移除有个原则,怎么注册的就要原封不动的 copy 参数调用移除。此处需要注意的是当你的事件处理函数是匿名函数时,那将会永远也清理不掉了。

this 指向

处理函数执行时,this 指向现代的浏览器指向的是 事件所在dom 节点,老 IE 指向window。

事件对象

事件发生后,会产生一个事件对象,作为参数传给监听函数。事件有若干的实例属性和实例方法。只讲下笔者认为重要的currentTargettargetpreventDefault().stopPropagation()stopImmediatePropagation()

currentTarget事件处理函数注册在什么节点上,那么 currentTarget 就永远的指向了该节点。
target,我们回顾一下事件流的概念,事件会经历捕获阶段、目标阶段、冒泡阶段,可以感觉到事件的传递画了个对称的钩,target 表示事件当前所处的节点位置。
我们不仅可以读取事件的状态,还可以人为的改变它的内部状态。

preventDefault()阻止事件的默认行为,如 a 标签被点击时就会跳转到新的 url,我们可以使用event.preventDefault()来阻止跳转。

stopPropagation()阻断事件的冒泡流或者事件捕获流。但如果同一节点上注册了多个事件处理程序,那么该节点上的事件处理程序会继续处理,它只会阻断事件向上或向下的传播。

stopImmediatePropagation()更为狠毒,事件会停止在该事件处理程序上,同节点的其他事件处理程序也不会被触发了。

事件代理

由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。

<ul>
    <li>test</li>
    <li>test</li>
    <li>test</li>
    <li>test</li>
    <li>test</li>
    ...
</ul>

假设业务需要给 li 添加 click 监听事件,那么初学者可能会采用直接获取 li 节点的方式进行事件程序的绑定,那么有几个 li,就需要注册多少个事件处理程序。可以采用事件代理机制实现性能优化,代码如下:

var ul = document.querySelector('ul');

ul.addEventListener('click', function (event) {
  if (event.target.tagName.toLowerCase() === 'li') {
    // some code
  }
});

事件代理和事件委托是同一个东西,主要是利用事件冒泡和事件的实例属性 target,目的是减少事件处理程序的数量,提高性能。

资料推荐

红宝书
阮一峰 事件模型
? 前端学习QQ群: 538631558 ?

qrcode_for_gh_3e207eccee76_258 (3).jpg


亖混子
4.7k 声望2.4k 粉丝

自信、自洽、自在