JS事件那些事儿 一次整明白

DOM 事件流

事件流包括三个阶段。简而言之:事件一开始从文档的根节点流向目标对象(捕获阶段),而后在目标对象上被触发(目标阶段),以后再回溯到文档的根节点(冒泡阶段)。javascript

DOM 事件流

事件捕获阶段(Capture Phase)

事件的第一个阶段是捕获阶段。事件从文档的根节点出发,随着 DOM 树的结构向事件的目标节点流去。途中通过各个层次的 DOM 节点,并在各节点上触发捕获事件,直到到达事件的目标节点。捕获阶段的主要任务是创建传播路径,在冒泡阶段,事件会经过这个路径回溯到文档跟节点。css

目标阶段(Target Phase)

当事件到达目标节点的,事件就进入了目标阶段。事件在目标节点上被触发,而后会逆向回流,直到传播至最外层的文档节点。html

冒泡阶段(Bubble Phase)

事件在目标元素上触发后,并不在这个元素上终止。它会随着 DOM 树一层层向上冒泡,直到到达最外层的根节点。也就是说,同一个事件会依次在目标节点的父节点,父节点的父节点...直到最外层的节点上被触发。java

冒泡过程很是有用。它将咱们从对特定元素的事件监听中释放出来,相反,咱们能够监听 DOM 树上更上层的元素,等待事件冒泡的到达。若是没有事件冒泡,在某些状况下,咱们须要监听不少不一样的元素来确保捕获到想要的事件。node

全部的事件都要通过捕捉阶段和目标阶段,可是有些事件会跳过冒泡阶段。例如,让元素得到输入焦点的 focus 事件以及失去输入焦点的 blur 事件就都不会冒泡。git

事件处理程序

HTML 事件处理程序

<!-- 输出 click -->
<input type="button" value="Click Me" onclick="console.log(event.type)">

<!-- 输出 Click Me this 值等于事件的目标元素 -->
<input type="button" value="Click Me" onclick="console.log(this.value)">
复制代码

固然在 HTML 中定义的事件处理程序也能够调用其它地方定义的脚本:web

<!-- Chrome 输出 click -->
<script> function showMessage(event) { console.log(event.type); } </script>
<input type="button" value="Click Me" onclick="showMessage(event)">
复制代码

经过 HTML 指定的事件处理程序都须要HTML的参与,即结构和行为相耦合,不易维护。chrome

DOM0 级事件处理程序

<input type="button" value="Click Me" id="btn">
<script> var btn=document.getElementById("btn"); btn.onclick=function(){ console.log(this.id); // 输出 btn } </script>
复制代码

这里是将一个函数赋值给一个事件处理程序的属性,以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。要删除事件将 btn.onclick 设置为 null 便可。segmentfault

DOM2 级事件处理程序

DOM2 级事件定义了addEventListener()removeEventListener()两个方法,用于处理和删除事件处理程序的操做。浏览器

全部 DOM 节点都包含这两个方法,它们接受3个参数:要处理的事件名做为事件处理程序的函数一个布尔值。最后的布尔值参数是 true 表示在捕获阶段调用事件处理程序,若是是 false(默认) 表示在冒泡阶段调用事件处理程序。

<input type="button" value="Click Me" id="btn">
<script> var btn=document.getElementById("btn"); btn.addEventListener("click",function(){ console.log(this.id); },false); btn.addEventListener("click",function(){ console.log('Hello word!'); },false); </script>
复制代码

上面代码两个事件处理程序会按照它们的添加顺序触发,先输出 btn 再输出 Hello word!

经过 addEventListener()添加的事件处理程序只能使用 removeEventListener()来移除,移除时传入的参数与添加时使用的参数相同,即匿名函数没法被移除。

<input type="button" value="Click Me" id="btn">
<script> var btn=document.getElementById("btn"); var handler = function(){ console.log(this.id); } btn.addEventListener("click", handler, false); btn.removeEventListener("click",handler, false); </script>
复制代码

IE 事件处理程序

IE一般都是特立独行的,它添加和删除事件处理程序的方法分别是:attachEvent()detachEvent()

一样接受事件处理程序名称与事件处理程序函数两个参数,但跟addEventListener()的区别是:

  • 事件名称须要加“on”,好比“onclick”;
  • 没了第三个布尔值,IE8及更早版本只支持事件冒泡;
  • 仍可添加多个处理程序,但触发顺序是反着来的。

还有一点须要注意,DOM0 和 DOM2 级的方法,其做用域都是在其所依附的元素当中,attachEvent()则是全局,即若是像以前同样使用this.id,访问到的就不是 button 元素,而是 window,就得不到正确的结果。

跨浏览器事件处理程序

直接上码:

var EventUtil={
    addHandler:function(element,type,handler){
        if(element.addEventListener){
            element.addEventListener(type,handler,false);
        } else if(element.attachEvent){
            element.attachEvent(“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,这个对象中包含着全部与事件有关的信息。全部的浏览器都支持 event 对象,但支持方式不一样。

DOM 中的事件对象

兼容 DOM 的浏览器会将一个 event 对象传入到事件处理程序中。event 对象包含与建立它的特定事件有关的属性和方法。触发的事件类型不同,可用的属性和方法也不同。

不过,全部事件都会有下面列出的成员。

  • bubbles (boolean) — 代表事件是否冒泡

  • cancelable (boolean) — 这个变量指明这个事件的默认行为是否能够经过调用 event.preventDefault 来阻止。也就是说,只有 cancelable 为 true 的时候,调用 event.preventDefault 才能生效。

  • currentTarget(element) — 当事件遍历DOM时,标识事件的当前目标。它老是引用事件处理程序附加到的元素,而不是event.target,event.target标识事件发生的元素。

  • defaultPrevented (boolean) — 这个状态变量代表当前事件对象的 preventDefault 方法是否被调用过

  • eventPhase (number) — 这个数字变量表示当前这个事件所处的阶段(phase):none(0),capture(1),target(2),bubbling(3)。

  • preventDefault(function) — 这个方法将阻止浏览器中用户代理对当前事件的相关默认行为被触发。好比阻止<a>元素的 click 事件加载一个新的页面

  • stopImmediatePropagation (function) — 这个方法将阻止当前事件链上全部的回调函数被触发,也包括当前节点上针对此事件已绑定的其余回调函数。

  • stopPropagation (function) — 阻止捕获和冒泡阶段中当前事件的进一步传播。

  • target (element) — 事件起源的 DOM 节点(获取标签名:ev.target.nodeName)

  • type (String) — 事件的名称

  • isTrusted (boolean) — 若是一个事件是由设备自己(如浏览器)触发的,而不是经过 JavaScript 模拟合成的,那个这个事件被称为可信任的(trusted)

  • timestamp (number) — 事件发生的时间

在事件处理程序内部,对象 this 始终等于 currentTarget 的值,而 target 则只包含事件的实际目标。若是直接将事件处理程序指定给了目标元素, 则 this、currentTarget、target 包含相同的值。

<input type="button" value="Click Me" id="btn">
<script> var btn=document.getElementById("btn"); btn.onclick = function (event) { console.log(event.currentTarget === this); // true console.log(event.target === this); // true } </script>
复制代码

若是事件处理程序存在于按钮的父节点,那么这些值是不一样的。

<input type="button" value="Click Me" id="btn">
<script> var btn=document.getElementById("btn"); document.body.onclick = function (event) { console.log(event.currentTarget === document.body); // true console.log(this === document.body); // true console.log(event.target === btn); // true } </script>
复制代码

在须要经过一个函数处理多个事件时,可使用 type 属性。

<input type="button" value="Click Me" id="btn">
<script> var btn=document.getElementById("btn"); var handler = function(event) { switch (event.type) { case "click": console.log("clicked"); break; case "mouseover": event.target.style.backgroundColor = "red"; break; case "mouseout": event.target.style.backgroundColor = ""; break; } } btn.onclick = handler; btn.onmouseover = handler; btn.onmouseout = handler; </script>
复制代码

跨浏览器的事件对象

万恶的 IE!直接上码:

EventUtil = {
    addHandler: function(element,type,handler){
        // 省略代码
    },
    removeHandler: function(element,type,handler){
        // 省略代码
    },
    getEvent: function(event){
        return event?event:window.event;
    },
    getTarget: function(event){
        return event.target || event.srcElement;
    },
    preventDefault: function(event){
        if(event.preventDefault){
            event.preventDefault();
        }else{
            event.returnValue = false;
        }
    },
    stopProgagation: function(event){
        if(event.stopProgagation){
            event.stopProgagation();
        }else{
            event.cancelBubble = true;
        }
    }
};
复制代码

更多详情参见:跨浏览器的事件对象

阻止事件冒泡/中止传播(Stopping Propagation)

能够经过调用事件对象的 stopPropagation 方法,在任何阶段(捕获阶段或者冒泡阶段)中断事件的传播。此后,事件不会在后面传播过程当中的通过的节点上调用任何的监听函数。

<input type="button" value="Click Me" id="btn">
<script> var btn=document.getElementById("btn"); btn.onclick = function (event) { console.log("Clicked"); // 触发 event.stopPropagation(); } document.body.onclick = function (event) { console.log("Body clicked"); // 传播阻断 不触发 } </script>
复制代码

调用 event.stopPropagation()不会阻止当前节点上此事件其余的监听函数被调用。若是你但愿阻止当前节点上的其余回调函数被调用的话,你可使用更激进的 event.stopImmediatePropagation()方法。

阻止浏览器默认行为

当特定事件发生的时候,浏览器会有一些默认的行为做为反应。最多见的事件不过于 link 被点击。当一个 click 事件在一个<a>元素上被触发时,它会向上冒泡直到 DOM 结构的最外层 document,浏览器会解释 href 属性,而且在窗口中加载新地址的内容。

在 web 应用中,开发人员常常但愿可以自行管理导航(navigation)信息,而不是经过刷新页面。为了实现这个目的,咱们须要阻止浏览器针对点击事件的默认行为,而使用咱们本身的处理方式。这时,咱们就须要调用 event.preventDefault().

咱们能够阻止浏览器的不少其余默认行为。好比,咱们能够在 HTML5 游戏中阻止敲击空格时的页面滚动行为,或者阻止文本选择框的点击行为。

调用 event.stopPropagation()只会阻止传播链中后续的回调函数被触发。它不会阻止浏览器的自身的行为。

事件类型

事件的种类可谓至关繁多,不一样的事件类型具备不一样的信息,经常使用的大体可分为以下几类:

  • UI:load、 error(错误触发)、select、resize、scroll等
  • 焦点:blur、focus、change(当用户提交对元素值的更改时触发)等
  • 鼠标:click、dblclick、mousedown/up、mouseenter/leave、mousemove、mouseover等

一些注意点:一、除 mouseenter/leave 全部鼠标事件都会冒泡; 二、只有在同一个元素上相继触发 mousedown/up 事件,才会触发 click 事件,只有触发两次 click 事件才会触发一次 dblclick 事件,4个事件的触发顺序为 mousedown → mouseup → click → mousedown → mouseup → click → dblclick (ie8及以前双击事件会跳过第二个 mousedown 和 click 事件);

  • 键盘:keydown/up、keypress
  • 触摸:touchstart(即便已经有一个手指放在屏幕上也会触发)、touchend、touchmove等

在触摸屏幕上的元素时事件的发生顺序为:touchstart → mouseover → mousemove(一次) → mousedown → mouseup → click → touchend;

  • 手势:gesturestart、gestureend、gesturechange等

一、gesturestart:当一个手指已经按在屏幕上,而另外一个手指又触摸在屏幕时触发。 二、gesturechange:当触摸屏幕的任何一个手指的位置发生变化时触发。三、gestureend:当任何一个手指从屏幕上面移开时触发。注意:只有两个手指都触摸到事件的接收容器时才触发这些手势事件。

  • 设备:orientationchange(检测设备屏幕旋转)、deviceorientation(检测设备方向变化)等

上面这些,大都是人为操做,还有些事件是网页状态带来的,好比:网页加载完成、提交表单、网页出错等。

除此以外,还有变更事件,复合事件,HTML5新加入的一些事件,再也不一一列出。完整列表可在这里查看 Web Events

内存和性能

事件委托/代理事件监听

事件委托利用了事件冒泡,只指定一个事件处理程序,就能够管理某一类型的全部事件。例如,click 事件会一直冒泡到 document 层次,也就是说,咱们能够为整个页面指定一个 onclick 事件处理程序,而没必要为每一个可点击的元素分别添加事件处理程序。

<ul id="myLinks">
    <li id="goSomewhere">Go somewhere</li>
    <li id="doSomething">Do something</li>
    <li id="sayHi">Say hi</li>
</ul>
<script> var list = document.getElementById("myLinks"); EVentUtil.addHandler (list, "click", function (event) { event = EVentUtil.getEvent(event); var target = EVentUtil.gitTarget(event); switch(target.id) { case "doSomething": document.title = "I changed the document's title"; break; case "goSomewhere": location.href = "https://heycoder.cn/"; break; case "sayHi": console.log("hi"); break; } }) </script>
复制代码

移除事件处理程序

大可能是 IE 的锅!

内存中留有那些过期不用的“空事件处理程序”,也是形成 web 应用程序内存与性能问题的主要缘由。

在两种状况下,可能会形成上述问题:

第一种状况就是从文档中移除带有事件处理程序的元素时。这多是经过纯粹的DOM操做,例如使用removeChild()replaceChild()方法,但更多地是发生在使用 innerHTML 替换页面中某一部分的时候。若是带有事件处理程序的元素被 innerHTML 删除了,那么原来添加到元素中的事件处理程序极有可能被看成垃圾回收。来看下面的例子:

<div id="myDiv">
    <input type="button" value="ClickMe" id="myBtn">
</div>
<script> var btn = document.getElementById("myBtn"); btn.onclick=function(){ document.getElementById("myDiv").innerHTML="Processing…"; } </script>
复制代码

这里,有一个按钮被包含在<div>元素中,为避免双击,单击这个按钮时就将按钮移除并替换成一条消息;这是网站设计中很是流行的一种作法。但问题在于,当按钮被从页面中移除时,它还带着一个事件处理程序呢,在<div>元素中设置 innerHTML 能够把按钮移走,但事件处理各类仍然与按钮保持着引用联系。有的浏览器(尤为是IE)在这种状况下不会做出恰当的处理,它们颇有可能会将对元素和事件处理程序的引用都保存在内存中。若是你想知道某个元即将被移除,那么最好手工移除事件处理程序。以下面的例子所示:

<div id="myDiv">
    <input type="button" value="ClickMe" id="myBtn">
</div>
<script> var btn = document.getElementById("myBtn"); btn.onclick=function(){ btn.onclick=null; document.getElementById("myDiv").innerHTML="Processing…"; } </script>
复制代码

在此,咱们设置<div>的innerHTML属性以前,先移除了按钮的事件处理程序。这样就确保了内存能够被再次利用,而从DOM中移除按钮也作到了干净利索。

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

致使“空事件处理程序”的另外一状况,就是卸载页面中的时候。绝不奇怪,IE在这种状况下依然是问题最多的浏览器,尽管其余浏览器或多或少也有相似的问题。若是在页面被卸载以前没有清理干净事件处理程序。那它们就会滞留在内存中。每次加载完页面再卸载页面时(多是在两个页面间来加切换,也能够是单击了“刷新”按钮),内存中滞留的对象数目就会增长,由于事件处理程序占用的内存并无被释放。

通常来讲,最好的作法是在页面卸载以前 ,先经过 onunload 事件处理程序移除全部事件处理程序。在此,事件委托技术再次表现出它的优点——须要跟踪的事件程序越少,移除它们就越容易,对这种相似的操做,咱们可把它想象成:只要是经过 onload 事件处理程序添加的东西,最后都要经过 onunload 事件处理程序将它们移除。

注:不要忘了,使用 onunload 事件处理程序意味着页面不会被缓存在 bfcachek 中,若是你在乎这个问题,那么就只能在 IE 中经过onunload 来移除事件处理程序了。

一些经常使用的操做

load 事件:

load 事件能够在任何资源(包括被依赖的资源)被加载完成时被触发,这些资源能够是图片,css,脚本,视频,音频等文件,也能够是 document 或者 window。

Image 元素 load:

// window onload
EVentUtil.addHandler (window, "load", function () {
    var image = new Image();
    // 要在指定 src 属性以前先指定事件
    EVentUtil.addHandler (image, "load", function () {
        console.log("Image loaded!");
    });
    image.src = "smile.gif";
})
复制代码

注意:新图像元素不必定要添加到文档后才开始下载,只要设置了 src 属性就会开始下载。

script 元素 load:

// window onload
EVentUtil.addHandler (window, "load", function () {
    var script = document.createElement("script");
    EVentUtil.addHandler (script, "load", function (event) {
        console.log("loaded!");
    });
    script.src = "example.js";
    document.body.appendChild(script);
})
复制代码

与图像不一样,只有设置了 script 元素的 src 属性并将元素添加到文档后,才会开始下载 js 文件,对于 script 元素而言指定 src 属性和指定事件处理程序的前后顺序就不重要了。

onbeforeunload 事件(HTML5事件):

window.onbeforeunload 让开发人员能够在想用户离开一个页面的时候进行确认。这个在有些应用中很是有用,好比用户不当心关闭浏览器的 tab,咱们能够要求用户保存他的修改和数据,不然将会丢失他此次的操做。

EVentUtil.addHandler (window, "onbeforeunload", function (event) {
    if (textarea.value != textarea.defaultValue) {
        return 'Do you want to leave the page and discard changes?';
    }
});
复制代码

须要注意的是,对页面添加 onbeforeunload 处理会致使浏览器不对页面进行缓存,这样会影响页面的访问响应时间。 同时,onbeforeunload 的处理函数必须是同步的(synchronous)。

resize 事件:

在一些复杂的响应式布局中,对 window 对象监听 resize 事件是很是经常使用的一个技巧。仅仅经过 css 来达到想要的布局效果比较困难。不少时候,咱们须要使用 JavaScript 来计算并设置一个元素的大小。

EVentUtil.addHandler (window, "resize", function (event) {
    // update the layout
});
复制代码

error 事件:

当咱们的应用在加载资源的时候发生了错误,咱们不少时候须要去作点什么,尤为当用户处于一个不稳定的网络状况下。Financial Times 中,咱们使用 error 事件来监测文章中的某些图片加载失败,从而马上隐藏它。因为“DOM Leven 3 Event”规定从新定义了 error 事件再也不冒泡,咱们可使用以下的两种方式来处理这个事件。

imageNode.addEventListener('error', function(event) {
    image.style.display = 'none';
});
复制代码

不幸的是,addEventListener 并不能处理全部的状况。而确保图片加载错误回调函数被执行的惟一方式是使用让人诟病内联事件处理函数(inline event handlers)。

<img src="http://example.com/image.jpg" onerror="this.style.display='none';" />
复制代码

缘由是你不能肯定绑定 error 事件处理函数的代码会在 error 事件发生以前被执行。而使用内联处理函数意味着在标签被解析而且请求图片的时候,error监听器也将并绑定。

获取鼠标在网页中的坐标:

From:js鼠标事件参数,获取鼠标在网页中的坐标

直接上码:

// 鼠标事件参数 兼容性封装 Test Already.
var EventUtil = {
    getEvent : function(e){
        return e || window.event;
    },

    getTarget : function(e){
        return this.getEvent(e).target || this.getEvent(e).srcElement;
    },

    getClientX : function(e){
        return this.getEvent(e).clientX;
    },

    getClientY : function(e){
        return this.getEvent(e).clientY;
    },

    // 水平滚动条偏移
    getScrollLeft : function(){
        return  document.documentElement.scrollLeft ||    // 火狐 IE9及如下滚动条是HTML的
                window.pageXOffset ||                     // IE10及以上 window.pageXOffset
                document.body.scrollLeft;                 // chrome 滚动条是body的
    },

    // 垂直滚动条偏移
    getScrollTop : function(){
        return  document.documentElement.scrollTop ||    // 火狐 IE9 及如下滚动条是 HTML 的
                window.pageYOffset ||                    // IE10 及以上 window.pageXOffset
                document.body.scrollTop;                 // chrome 滚动条是body的,不过目前新版的滚动条也放到 document上去了
    },

    getPageX : function(e){
        return (this.getEvent(e).pageX)?( this.getEvent(e).pageX ):( this.getClientX(e)+this.getScrollLeft() );
    },

    getPageY : function(e){
        return (this.getEvent(e).pageY)?( this.getEvent(e).pageY ):( this.getClientY(e)+this.getScrollTop() );
    }
};
复制代码

附加

关于Chrome浏览器 document.body.scrollTop 一直为0的问题可参考这里

写在后面的话:对于 js 事件总会有一种朦胧美的感受,最近也算没太忙,索性再翻看下 js 高程,本篇文章算是高程事件章节的读书笔记,也会有所扩展。

相关文章
相关标签/搜索