Javascript事件系统
事件基础
注意:本文不会深入探究javascript的事件循环。
提到事件,相信每位javascript开发者都不会陌生,由于javascript是先有实现,后有规范,因此,对于大部分人来说,事件模块可以说是比较模糊的,本文将从不同角度帮助你理清楚事件模块。
事件的本质可以说是一个回调函数,当事件触发时会调用你的监听函数。
事件是一定会触发的,如果没有对应的监听函数,就不会执行回调。
比如下面就是用户点击指定元素打印日志的例子:
document.queryselector('#button').onclick = function() { console.log('clicked'); };
事件基础相信大家都没什么问题,重点在后面的内容。
事件监听方式
由于历史原因,javascript目前存在三种事件监听方式:
- html代码中监听
- dom0级监听
- dom2级监听
q: 为啥从dom0级开始?
1998年,w3c综合各浏览器厂商的现有api,指定了dom1标准。在dom1标准出现之前浏览器已有的事件监听方式叫做dom0级。
q:dom1级监听到哪里去了?
由于dom1标准只是对dom0标准的整理+规范化,并没有增加新的内容,因此dom0级可以看做dom1级。
html代码监听
<button onclick="alert('hello world!')">点我</button>
直接将事件处理函数或事件处理代码写到html元素对应的属性上的方式就是html代码监听方式。
该方式有一个明显的缺点,如果事件逻辑比较复杂时,将大段代码直接写在html元素上不利于维护。因此一般会提取到一个专一的函数进行处理。
<button onclick="callback()">点我</button>
该方式也有一个问题,那就是如果callback()
函数还未加载好时点击按钮将报错。而且直接将事件耦合到html元素上也不符合单一职责,html元素应该只负责展示,不负责事件。
不建议在开发中使用该方式处理事件。
dom0级事件监听
在dom1级规范出来之前,各浏览器厂商已经提供了一套事件api,也就是dom0级api,它的写法如下:
<button id="click">点我</button><script> document.queryselector('#click').onclick = function() { console.log('clicked'); };</script>
这个相信大家在刚开始入行时写的比较多,比如我们的ajax相关api就是dom0级的。
var xhr = new xmlhttprequest(); xhr.onload = function() {}; xhr.onerror = function() {};
dom0级事件基本上都是以"on"开头的
dom0级事件也存在一个问题,那就是不支持添加多个事件处理函数,因此只有在不支持dom2级事件的情况下才会使用dom0级来绑定事件。
dom2级事件监听
dom2级事件是最新的事件处理程序规范(有许多年未更新了)。dom2级事件通过addeventlistener
方式给元素添加事件处理程序。
<button id="click">点我</button><script> document.addeventlistener('click', function(){ console.log('clicked'); });</script>
多次调用addeventlistener可以绑定多个事件处理程序,但是需要注意:
同样的事件名、同样的事件处理函数和同样的事件流机制(冒泡和捕获,下面会讲到),只会触发一次。
// 下面的代码只会触发一次<button id="request">登录</button><script>function onclick() { console.log('clicked'); }document.queryselector('#request').addeventlistener('click', onclick, false);document.queryselector('#request').addeventlistener('click', onclick, false);</script>
onclick是同一个事件处理程序,所以只触发一次
// 下面的代码只会触发两次<button id="request">登录</button> <script> document.queryselector('#request').addeventlistener('click', function() { console.log('clicked'); }, false); document.queryselector('#request').addeventlistener('click', function() { console.log('clicked'); }, false); </script>
两个匿名函数,所以会触发两次
事件默认行为
很多网页元素会有默认行为,比如下面这些:
- 点击a标签的时候,会有跳转行为
- 点击右键时会弹出菜单
- 在表单中点击提交按钮会提交表单
如果我们需要阻止默认行为,比如我们在阻止表单的默认提交事件,进行数据校验,通过校验后再调用表单submit方法提交。
不同的监听方式阻止默认行为的方式也不同。
html代码方式
html代码方式支持return false和event.preventdefault()
return false方式
<form action="" onsubmit="return handlesubmit()"> <button type="submit">submit</button></form><script>function handlesubmit() { return false; }</script>
上例中我们监听了表单的onsubmit
事件,当点击按钮或者按下回车时,将会触发handlesubmit
方法,同时会阻止表单的提交。
表单内如果有type="submit"的按钮存在,按下回车时就会自动提交。
html监听方式阻止默认事件需要满足以下两点:
-
html事件监听代码
return handler()
,return不能少
,少了就无法阻止默认行为 -
handler()
函数需要返回false
event.preventdefault()
<a href="https://www.ddhigh.com" onclick="handleclick(event)" id="click">href</a><script>function handleclick(e) { e.preventdefault(); }</script>
dom0级事件方式
dom0级事件支持return false和event.preventdefault()两种方式。
event.preventdefault()
// event.preventdefault()<a href="https://www.ddhigh.com" id="click">href</a><script> document.queryselector('#click').onclick= function (event) { event.preventdefault(); };</script>
return false
// return false<a href="https://www.ddhigh.com" id="click">href</a><script> document.queryselector('#click').onclick= function (event) { return false; };</script>
两种方式都能工作,不过建议使用event.preventdefault()
,原因在下面dom2级会讲到
dom2级事件
dom2级事件事件只支持event.preventdefault()方式,这也是事件的标准处理方法。
<a href="https://www.ddhigh.com" id="click">href</a><script>document.queryselector('#click').addeventlistener('click', function (e) { e.preventdefault(); });</script>
事件冒泡与事件捕获
先来看一个html结构
<div id="father"> <div id="child"> <div id="son">click</div> </div></div>
我们知道,一旦绑定了事件处理程序,在事件触发时,事件处理函数都会触发。
如果我们给father/child/son都绑定了事件处理函数,点击了son时,谁被触发呢?
事实上,三个函数都会被触发,因为son时child的子元素,child又是father的子元素,点击son,同时也点击了father和child。
由此带来一个问题,三个函数谁先触发,谁后触发呢?这就是我们常说的事件流,father->child->son这种路径是可以的,但是son->child->father这种路径也是可以的。
针对这两种方式,w3c给了我们一个答案,两种方式都支持,即可以从父元素到子元素,又可以从子元素到父元素,前者叫事件捕获,后者叫事件冒泡。
事件捕获
事件发生时采取自上而下
的方式进行触发,最先触发的是window
,其次是document
,然后根据dom层级依次触发,最终进入到真正的事件元素。
addeventlistener第三个参数传入true就是捕获方式的标志。
<div id="father"> <div id="child"> <div id="son">click</div> </div> </div> <script> document.queryselector('#father').addeventlistener('click', function () { console.log('father'); }, true); document.queryselector('#child').addeventlistener('click', function () { console.log('child'); }, true); document.queryselector('#son').addeventlistener('click', function () { console.log('son'); }, true); </script>
点击son之后的输出顺序为
father child son
事件冒泡
事件发生时采取自下而上
的方式进行触发,最先触发的是发生事件的元素,其次是父元素,依次向上,最终触发到document
和window
。
addeventlistener第三个参数传入false就是事件冒泡的标志。
<div id="father"> <div id="child"> <div id="son">click</div> </div> </div> <script> document.queryselector('#father').addeventlistener('click', function () { console.log('father'); }, false); document.queryselector('#child').addeventlistener('click', function () { console.log('child'); }, false); document.queryselector('#son').addeventlistener('click', function () { console.log('son'); }, false); </script>
点击son之后的输出顺序为
son child father
由于事件捕获和事件冒泡机制,我们需要一个标记来标识真正触发事件的元素,这个元素就是event.target,而另外一个相似的属性叫event.currenttarget,这是当前元素。
事件捕获和时间冒泡的顺序
根据浏览器规范,事件捕获会先于事件冒泡发生。因此,总的事件顺序如下
- window 捕获阶段
- document 捕获阶段
- ... 依次到真正触发事件的元素 捕获阶段
- 真正触发事件的元素 冒泡阶段
- 依次向上的父元素 冒泡阶段
- document 冒泡阶段
- window 冒泡阶段
<div id="father"> <div id="child"> <div id="son">click</div> </div> </div> <script> document.queryselector('#father').addeventlistener('click', function () { console.log('father捕获'); }, true); document.queryselector('#child').addeventlistener('click', function () { console.log('child捕获'); }, true); document.queryselector('#son').addeventlistener('click', function () { console.log('son捕获'); }, true); document.queryselector('#father').addeventlistener('click', function () { console.log('father冒泡'); }, false); document.queryselector('#child').addeventlistener('click', function () { console.log('child冒泡'); }, false); document.queryselector('#son').addeventlistener('click', function () { console.log('son冒泡'); }, false); </script>
点击son之后的输出为
father捕获 child捕获 son捕获 son冒泡 child冒泡 father冒泡
事件绑定和事件委托
弄明白浏览器的事件流机制之后,来讨论事件绑定和事件委托其实是很简单的事情。
事件绑定
就是在事件监听方式中直接对具体元素进行事件监听的方式。有个明显的缺点,对于新增加的dom节点是无法监听到事件的。
<div class="a">click1</div> <div class="a">click2</div> <script> document.queryselectorall('.a').foreach(ele => ele.onclick = function () { console.log('clicked ' + this.innerhtml); }); settimeout(function () { const div3 = document.createelement('div') div3.classname = "a"; div3.innerhtml = "click3" document.body.appendchild(div3) }, 500); </script>
上面的click3点击是没有任何反应的,因为在创建该元素时没有绑定事件处理函数。
事件委托
我们利用事件流机制来实现上面的需求。
事件委托就是利用事件流机制,在父元素进行监听,由于事件冒泡机制,父元素可以接受新添加元素的事件。
<div class="a">click1</div> <div class="a">click2</div> <script> document.body.addeventlistener('click', function (e) { console.log(e.target.innerhtml) }, false); settimeout(function () { const div3 = document.createelement('div') div3.classname = "a"; div3.innerhtml = "click3" document.body.appendchild(div3) }, 500); </script>
由于事件冒泡机制,click3元素点击之后会将事件冒泡给父元素,也就是我们的document.body,通过event.target可以拿到真正触发事件的元素。
推荐阅读
-
Win 10系统下怎么安装M1213打印机?安装M1213打印机的方法
-
JavaScript去掉数组重复项的方法分析【测试可用】
-
用纯Node.JS弹出Windows系统消息提示框实例(MessageBox)
-
JavaScript手风琴页面制作
-
12个非常有用的JavaScript技巧
-
JavaScript使用类似break机制中断forEach循环的方法
-
JavaScript的深入理解:变量对象(Variable Object)
-
你应该了解的JavaScript Array.map()五种用途小结
-
Javascript特效学习之选项卡卡定时自动切换代码教程
-
javascript中一些奇葩的日期换算方法总结