本文主要介绍:javascript
针对不一样级别的DOM,咱们的DOM事件处理方式也是不同的。css
DOM级别一共能够分为4个级别:DOM0级「一般把DOM1规范造成以前的叫作DOM0级」,DOM1级,DOM2级和 DOM3级,而DOM事件分为3个级别:DOM0级事件处理,DOM2级事件处理和DOM3级事件处理。以下图所示:html
在了解DOM0级事件以前,咱们有必要先了解下HTML事件处理程序,也是最先的这一种的事件处理方式,代码以下:java
<button type="button" onclick="fn" id="btn">点我试试</button>
<script> function fn() { alert('Hello World'); } </script>
复制代码
那有一个问题来了,那就是fn要不要加括号呢?node
在html的onclick属性中,使用时要加括号,在js的onclick中,给点击事件赋值,不加括号。为何呢?咱们经过事实来讲话:segmentfault
// fn不加括号
<button type="button" onclick="fn" id="btn">点我试试</button>
<script> function fn() { alert('Hello World'); } console.log(document.getElementById('btn').onclick); // 打印的结果以下:这个函数里面包括着fn,点击以后并无弹出1 /* ƒ onclick(event) { fn } */ </script>
// fn 加括号,这里就不重复写上面代码,只须要修改一下上面便可
<button type="button" onclick="fn()" id="btn">点我试试</button>
<script> // 打印的结果以下:点击以后能够弹出1 /* ƒ onclick(event) { fn() } */ </script>
复制代码
上面的代码咱们经过直接在HTML代码当中定义了一个onclick的属性触发fn方法,这样的事件处理程序最大的缺点就是HTML与JS强耦合,当咱们一旦须要修改函数名就得修改两个地方。固然其优势就是不须要操做DOM来完成事件的绑定。浏览器
DOM0事件绑定,给元素的事件行为绑定方法,这些方法都是在当前元素事件行为的冒泡阶段(或者目标阶段)执行的。dom
那咱们如何实现HTML与JS低耦合?这样就有DOM0级处理事件的出现解决这个问题。DOM0级事件就是将一个函数赋值给一个事件处理属性,好比:编辑器
<button id="btn" type="button"></button>
<script> var btn = document.getElementById('btn'); btn.onclick = function() { alert('Hello World'); } // btn.onclick = null; 解绑事件 </script>
复制代码
上面的代码咱们给button定义了一个id,而后经过JS获取到了这个id的按钮,并将一个函数赋值给了一个事件处理属性onclick,这样的方法即是DOM0级处理事件的体现。咱们能够经过给事件处理属性赋值null来解绑事件。DOM 0级的事件处理的步骤:先找到DOM节点,而后把处理函数赋值给该节点对象的事件属性。函数
DOM0级事件处理程序的缺点在于一个处理程序「事件」没法同时绑定多个处理函数,好比我还想在按钮点击事件上加上另一个函数。
var btn = document.getElementById('btn');
btn.onclick = function() {
alert('Hello World');
}
btn.onclick = function() {
alert('没想到吧,我执行了,哈哈哈');
}
复制代码
DOM2级事件在DOM0级事件的基础上弥补了一个处理程序没法同时绑定多个处理函数的缺点,容许给一个处理程序添加多个处理函数。也就是说,使用DOM2事件能够随意添加多个处理函数,移除DOM2事件要用removeEventListener。代码以下:
<button type="button" id="btn">点我试试</button>
<script> var btn = document.getElementById('btn'); function fn() { alert('Hello World'); } btn.addEventListener('click', fn, false); // 解绑事件,代码以下 // btn.removeEventListener('click', fn, false); </script>
复制代码
DOM2级事件定义了addEventListener和removeEventListener两个方法,分别用来绑定和解绑事件
target.addEventListener(type, listener[, useCapture]);
target.removeEventListener(type, listener[, useCapture]);
/* 方法中包含3个参数,分别是绑定的事件处理属性名称(不包含on)、事件处理函数、是否在捕获时执行事件处理函数(关于事件冒泡和事件捕获下面会介绍) */
复制代码
注:
IE8级如下版本不支持addEventListener和removeEventListener,须要用attachEvent和detachEvent来实现:
// IE8级如下版本只支持冒泡型事件,不支持事件捕获因此没有第三个参数
// 方法中包含2个参数,分别是绑定的事件处理属性名称(不包含on)、事件处理函数
btn.attachEvent('onclick', fn); // 绑定事件
btn.detachEvent('onclick', fn); // 解绑事件
复制代码
DOM3级事件在DOM2级事件的基础上添加了更多的事件类型,所有类型以下:
同时DOM3级事件也容许使用者自定义一些事件。
DOM事件级别的发展使得事件处理更加完整丰富,而下一个问题就是以前提到的DOM事件模型。「事件冒泡和事件捕获」
假如在一个button上注册了一个click事件,又在其它父元素div上注册了一个click事件,那么当咱们点击button,是先触发父元素上的事件,仍是button上的事件呢,这就须要一种约定去规范事件的执行顺序,就是事件执行的流程。
浏览器在发展的过程当中出现了两种不一样的规范
DOM事件模型分为捕获和冒泡。一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分红三个阶段。
(1)捕获阶段:事件从window对象自上而下向目标节点传播的阶段;
(2)目标阶段:真正的目标节点正在处理事件的阶段;
(3)冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。
上文中讲到了addEventListener的第三个参数为指定事件是否在捕获或冒泡阶段执行,设置为true表示事件在捕获阶段执行,而设置为false表示事件在冒泡阶段执行。那么什么是事件冒泡和事件捕获呢?能够用下图来解释:
捕获是从上到下,事件先从window对象,而后再到document(对象),而后是html标签(经过document.documentElement获取html标签),而后是body标签(经过document.body获取body标签),而后按照普通的html结构一层一层往下传,最后到达目标元素。咱们只须要将addEventListener的第三个参数改成true就能够实现事件捕获。代码以下:
<!-- CSS 代码 -->
<style> body{margin: 0;} div{border: 1px solid #000;} #grandfather1{width: 200px;height: 200px;} #parent1{width: 100px;height: 100px;margin: 0 auto;} #child1{width: 50px;height: 50px;margin: 0 auto;} </style>
<!-- HTML 代码 -->
<div id="grandfather1">
爷爷
<div id="parent1">
父亲
<div id="child1">儿子</div>
</div>
</div>
<!-- JS 代码 -->
<script> var grandfather1 = document.getElementById('grandfather1'), parent1 = document.getElementById('parent1'), child1 = document.getElementById('child1'); grandfather1.addEventListener('click',function fn1(){ console.log('爷爷'); },true) parent1.addEventListener('click',function fn1(){ console.log('爸爸'); },true) child1.addEventListener('click',function fn1(){ console.log('儿子'); },true) /* 当我点击儿子的时候,我是否点击了父亲和爷爷 当我点击儿子的时候,三个函数是否调用 */ // 请问fn1 fn2 fn3 的执行顺序? // fn1 fn2 fn3 or fn3 fn2 fn1 </script>
复制代码
先来看结果吧:
当咱们点击id为child1的div标签时,打印的结果是爷爷 => 爸爸 => 儿子,结果正好与事件冒泡相反。
所谓事件冒泡就是事件像泡泡同样从最开始生成的地方一层一层往上冒。咱们只须要将addEventListener的第三个参数改成false就能够实现事件冒泡。代码以下:
//html、css代码同上,js代码只是修改一下而已
var grandfather1 = document.getElementById('grandfather1'),
parent1 = document.getElementById('parent1'),
child1 = document.getElementById('child1');
grandfather1.addEventListener('click',function fn1(){
console.log('爷爷');
},false)
parent1.addEventListener('click',function fn1(){
console.log('爸爸');
},false)
child1.addEventListener('click',function fn1(){
console.log('儿子');
},false)
/* 当我点击儿子的时候,我是否点击了父亲和爷爷 当我点击儿子的时候,三个函数是否调用 */
// 请问fn1 fn2 fn3 的执行顺序?
// fn1 fn2 fn3 or fn3 fn2 fn1
复制代码
先来看结果吧:
好比上图中id为child1的div标签为事件目标,点击以后后同时也会触发父级上的点击事件,一层一层向上直至最外层的html或document。
注:当第三个参数为false
或者为空的时候,表明在冒泡阶段绑定。
因为事件会在冒泡阶段向上传播到父节点,所以能够把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫作事件的代理(delegation)。
举个例子,好比一个宿舍的同窗同时快递到了,一种方法就是他们都傻傻地一个个去领取,还有一种方法就是把这件事情委托给宿舍长,让一我的出去拿好全部快递,而后再根据收件人一一分发给每一个宿舍同窗;
在这里,取快递就是一个事件,每一个同窗指的是须要响应事件的 DOM 元素,而出去统一领取快递的宿舍长就是代理的元素,因此真正绑定事件的是这个元素,按照收件人分发快递的过程就是在事件执行中,须要判断当前响应的事件应该匹配到被代理元素中的哪个或者哪几个。
那么利用事件冒泡或捕获的机制,咱们能够对事件绑定作一些优化。 在JS中,若是咱们注册的事件愈来愈多,页面的性能就愈来愈差,由于:
假设有一个列表,列表之中有大量的列表项,咱们须要在点击每一个列表项的时候响应一个事件
// 例4
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>
复制代码
若是给每一个列表项一一都绑定一个函数,那对于内存消耗是很是大的,效率上须要消耗不少性能。借助事件代理,咱们只须要给父容器ul绑定方法便可,这样无论点击的是哪个后代元素,都会根据冒泡传播的传递机制,把容器的click行为触发,而后把对应的方法执行,根据事件源,咱们能够知道点击的是谁,从而完成不一样的事。
在不少时候,咱们须要经过用户操做动态的增删列表项元素,若是一开始给每一个子元素绑定事件,那么在列表发生变化时,就须要从新给新增的元素绑定事件,给即将删去的元素解绑事件,若是用事件代理就会省去不少这样麻烦。
接下来咱们来实现上例中父层元素 #list 下的 li 元素的事件委托到它的父层元素上:
<ul id="list">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
<script> // 给父层元素绑定事件 document.getElementById('list').addEventListener('click', function (e) { // 兼容性处理 var event = e || window.event; var target = event.target || event.srcElement; // 判断是否匹配目标元素 if (target.nodeName.toLocaleLowerCase() === 'li') { console.log('the content is: ', target.innerHTML); } }); </script>
复制代码
这是常规的实现事件委托的方法,可是这种方法有BUG,当监听的元素里存在子元素时,那么咱们点击这个子元素事件会失效,因此咱们能够联系文章上一小节说到的冒泡事件传播机制来解决这个bug。改进的事件委托代码:
<ul id="list">
<li>1 <span>aaaaa</span></li>
<li>2 <span>aaaaa</span></li>
<li>3 <span>aaaaa</span></li>
<li>4</li>
</ul>
<script> // 给父层元素绑定事件 document.getElementById('list').addEventListener('click', function (e) { // 兼容性处理 var event = e || window.event; var target = event.target || event.srcElement; // 判断是否匹配目标元素 /* 从target(点击)元素向上找currentTarget(监听)元素, 找到了想委托的元素就触发事件,没找到就返回null */ while(target.tagName !== 'LI'){ if(target.tagName === 'UL'){ target = null break; } target = target.parentNode } if (target) { console.log('你点击了ul里的li') } }); 复制代码
若是调用这个方法,默认事件行为将再也不触发。什么是默认事件呢?例如表单一点击提交按钮(submit)刷新页面、a标签默认页面跳转或是锚点定位等。
使用场景1:使用a标签仅仅是想当作一个普通的按钮,点击实现一个功能,不想页面跳转,也不想锚点定位。
<a href="javascript:;">连接</a>
复制代码
使用JS方法来阻止,给其click事件绑定方法,当咱们点击A标签的时候,先触发click事件,其次才会执行本身的默认行为
<a id="test" href="http://www.google.com">连接</a>
<script> test.onclick = function(e){ e = e || window.event; return false; } </script>
复制代码
<a id="test" href="http://www.google.com">连接</a>
<script> test.onclick = function(e){ e = e || window.event; e.preventDefault(); } </script>
复制代码
使用场景2:输入框最多只能输入六个字符,如何实现?
实现代码以下:
<input type="text" id='tempInp'>
<script> tempInp.onkeydown = function(ev) { ev = ev || window.event; let val = this.value.trim() //trim去除字符串首位空格(不兼容) // this.value=this.value.replace(/^ +| +$/g,'') 兼容写法 let len = val.length if (len >= 6) { this.value = val.substr(0, 6); //阻止默认行为去除特殊按键(DELETE\BACK-SPACE\方向键...) let code = ev.which || ev.keyCode; if (!/^(46|8|37|38|39|40)$/.test(code)) { ev.preventDefault() } } } </script>
复制代码
event.stopPropagation() 方法阻止事件冒泡到父元素,阻止任何父事件处理程序被执行。demo代码以下:
// 在事件冒泡demo代码的基础上修改一下
child1.addEventListener('click',function fn1(e){
console.log('儿子');
e.stopPropagation()
},false)
复制代码
stopImmediatePropagation 既能阻止事件向父元素冒泡,也能阻止元素同事件类型的其它监听器被触发。而 stopPropagation 只能实现前者的效果。咱们来看个例子:
<button id="btn">点我试试</button>
<script> const btn = document.querySelector('#btn'); btn.addEventListener('click', event => { console.log('btn click 1'); event.stopImmediatePropagation(); }); btn.addEventListener('click', event => { console.log('btn click 2'); }); document.body.addEventListener('click', () => { console.log('body click'); }); </script>
复制代码
根据打印出来的结果,咱们发现使用 stopImmediatePropagation后,点击按钮时,不只body绑定事件不会触发,与此同时按钮的另外一个点击事件也不触发。
从上面这张图片中咱们能够看到,event.target
指向引发触发事件的元素,而event.currentTarget
则是事件绑定的元素。
所以没必要记何时e.currentTarget
和e.target
相等,何时不等,理解二者的究竟指向的是谁便可。
e.target
指向触发事件监听的对象「事件的真正发出者」。e.currentTarget
指向添加监听事件的对象「监听事件者」。