注意:本文不会深刻探究Javascript的事件循环。
提到事件,相信每位Javascript开发者都不会陌生,因为Javascript是先有实现,后有规范,所以,对于大部分人来讲,事件模块能够说是比较模糊的,本文将从不一样角度帮助你理清楚事件模块。 javascript
事件的本质能够说是一个回调函数,当事件触发时会调用你的监听函数。 java
事件是必定会触发的,若是没有对应的监听函数,就不会执行回调。
好比下面就是用户点击指定元素打印日志的例子: ajax
document.querySelector('#button').onclick = function() { console.log('clicked'); };
事件基础相信你们都没什么问题,重点在后面的内容。 浏览器
因为历史缘由,Javascript目前存在三种事件监听方式: app
Q: 为啥从DOM0级开始? 函数
1998年,W3C综合各浏览器厂商的现有API,指定了DOM1标准。在DOM1标准出现以前浏览器已有的事件监听方式叫作DOM0级。
Q:DOM1级监听到哪里去了? this
因为DOM1标准只是对DOM0标准的整理+规范化,并无增长新的内容,所以DOM0级能够看作DOM1级。
<button onclick="alert('Hello World!')">点我</button>
直接将事件处理函数或事件处理代码写到HTML元素对应的属性上的方式就是HTML代码监听方式。 spa
该方式有一个明显的缺点,若是事件逻辑比较复杂时,将大段代码直接写在HTML元素上不利于维护。所以通常会提取到一个专注的函数进行处理。 日志
<button onclick="callback()">点我</button>
该方式也有一个问题,那就是若是callback()
函数还未加载好时点击按钮将报错。并且直接将事件耦合到HTML元素上也不符合单一职责,HTML元素应该只负责展现,不负责事件。 code
不建议在开发中使用该方式处理事件。
在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级事件经过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>
两个匿名函数,因此会触发两次
不少网页元素会有默认行为,好比下面这些:
若是咱们须要阻止默认行为,好比咱们在阻止表单的默认提交事件,进行数据校验,经过校验后再调用表单submit方法提交。
不一样的监听方式阻止默认行为的方式也不一样。
HTML代码方式支持return false和event.preventDefault()
<form action="" onsubmit="return handleSubmit()"> <button type="submit">Submit</button></form><script>function handleSubmit() { return false; }</script>
上例中咱们监听了表单的onsubmit
事件,当点击按钮或者按下回车时,将会触发handleSubmit
方法,同时会阻止表单的提交。
表单内若是有type="submit"的按钮存在,按下回车时就会自动提交。
HTML监听方式阻止默认事件须要知足如下两点:
return handler()
,return不能少
,少了就没法阻止默认行为 handler()
函数须要返回false
<a href="https://www.ddhigh.com" onclick="handleClick(event)" id="click">Href</a><script>function handleClick(e) { e.preventDefault(); }</script>
DOM0级事件支持return false和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<a href="https://www.ddhigh.com" id="click">Href</a><script> document.querySelector('#click').onclick= function (event) { return false; };</script>
两种方式都能工做,不过建议使用event.preventDefault()
,缘由在下面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,这是当前元素。
根据浏览器规范,事件捕获会先于事件冒泡发生。所以,总的事件顺序以下
<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能够拿到真正触发事件的元素。