EventTarget接口
事件的本质是程序各个组成部分之间的一种通信方式,也是异步编程的一种实现。
DOM的事件操作(监听和触发),都定义在EventTarget接口,该接口主要提供三个实例方法:
addEventListener:绑定事件监听函数removeEventListerer:移出事件监听函数dispatchEvent:触发事件
EventTarget.addEventListener()
在当前节点上定义一个特定时间的监听函数,一旦事件发生就会执行监听函数,该方法没有返回值。
target.addEventListener(type, listener[, useCapture]);
接收三个参数:
- type:事件名称
- listener:监听函数
- useCapture:布尔值,表示监听函数是否在捕获阶段触发
例:
function hello() {
console.log('你好');
}
let button = document.querySelector('#btn');
button.addEventListener('click',hello,false);
上面代码中 button 节点的addEventListener 方法绑定click事件的监听函数 hello() 该函数只在冒泡阶段触发
如果希望事件监听函数只执行一次,可以打开属性配置对象的 once属性
element.addEventListener('click', function (event) {
// 只执行一次的代码
}, {once: true});
EventTarget.removeEventListener()
用来移除addEventListener添加的监听函数,参数与addEventListener完全一致。注意:移除的函数必须是addEventListener方法添加的监听函数,而且必须在同一个元素节点,否则无效。
事件模型
浏览器的事件模型就是通过监听函数对事件做出反应,事件发生 后浏览器监听到了事件,就会执行对应的函数。JavaScript有三种方法可以为事件绑定监听函数。
监听函数
第一种:HTML的on-属性
可以直接在元素的属性中直接定义一些事件的监听代码。
<div onclick="alert('触发事件')"></div>
这段代码就给div添加了一个点击事件。元素的事件监听属性都是 on上加事件名,例如 onload就是on+load,表示load事件的监听代码。! 需要注意的是,这些属性的值是将会执行的代码,而不是函数。
使用这个方法指定的监听代码,只会在冒泡阶段触发。
例:
<body>
<div class="div01" onclick="console.log(3)">
<div class="div02" onclick="console.log(2)">
<button onclick="console.log(1)">点击</button>
</div>
</div>
</body>
点击 button控制台输出1 2 3
点击 div02控制台输出2 3
点击 div01控制台输出 3
button是div的子元素,所以 button的 click事件也会触发div的 click事件,由于 on-属性的监听代码只在冒泡阶段触发,所以点击结果先1,再2,再3,即事件从子元素冒泡到父元素。
元素节点的事件属性
例:
div.onclick = function() {
console.log('触发事件');
};
这个方法也只会在冒泡阶段触发。
EventTarget.addEventListener()
所有 DOM 节点实例都有addEventListener方法,用来为该节点定义事件的监听函数。
window.addEventListener('load', doSomething, false);一般来说我们推荐使用这种方法,第一种违反了HTML与JavaScript相分离的原则,第二种同一个事件指定定义一个监听函数。EventTarget.addEventListener()有一下优点:
- 同一事件可以添加多个监听函数
- 能够指定是在那个阶段(冒泡还是捕获)阶段触发监听函数
- 除了DOM节点,其他对象例如window,XMLHttpReques也有这个接口,等于是整个JavaScript统一的监听函数接口
this的指向
<button id="btn" onclick="console.log(this.id)">点击</button>
输出 btn
事件的传播
一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。
- 事件捕获阶段,先由文档的根节点 document 往事件触发对象,从外向内捕获事件对象;也就是从上层传到底层。
- 目标阶段,寻找到目标事件位置(事发地),触发事件,就是在目标节点上触发
- 事件冒泡阶段,再从目标事件位置往文档的根节点方向回溯,从内向外冒泡事件对象,也就是从目标节点传导回去,从底层传导到上层。
例:
<div>
<p>点击</p>
</div>
------------------------------------------
var phases = {
1: 'capture',
2: 'target',
3: 'bubble'
};
let div = document.querySelector('div');
let p = document.querySelector('p');
div.addEventListener('click', callback, true);
p.addEventListener('click', callback, true);
div.addEventListener('click', callback, false);
p.addEventListener('click', callback, false);
function callback(event) {
let tag = event.currentTarget.tagName;
let phase = phases[event.eventPhase];
console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'");
}
// 点击以后的结果
// Tag: 'DIV'. EventPhase: 'capture'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'DIV'. EventPhase: 'bubble'
//如果点击div,则会输出两次//Tag: 'DIV'. EventPhase: 'target'
click事件被触发了四次,div节点的捕获阶段和冒泡阶段各一次,p节点的目标阶段触发了两次
- 捕获阶段:事件从div向p传播时,触发div的click事件;
- 目标阶段:事件从div到达p时,触发p的click事件;
- 冒泡阶段:事件从p传回div时,再次触发div的click事件。
事件代理
由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点,由父节点的监听函数统一处理多个子元素的事件。原理就是时间的冒泡原理。类似于公司前台代为取快递,不需要大家都在门口等着取快递。
为什么使用事件委托
在JavaScript中添加到页面上的事件处理程序数量会直接关系到页面整体运行性能,因为需要不断与DOM节点交互,访问DOM的次数越多,浏览器重绘与重拍的次数也就越多(性能优化减少DOM操作),如果使用事件委托,可以将操作放到JS当中,与DOM的操作只需要交互一次,从而减少交互次数,提高性能。
事件代理的实现
一个简单的实现:
<ul id="ul1">
<li>1111111</li>
<li>2222222</li>
<li>3333333</li>
</ul>
-----------------------
<script>
let ul1 = document.querySelector('ul1');
ul1.onclick = function() {
alert('点击事件')
}
</script>
当 <li>被点击的时候事件就会冒泡到 ul上,所以就触发了点击事件,但是如果让事件代理的效果和直接点击节点的事件效果一样,就是只点击li才会触发,该怎么办?
Event提供了一个属性叫 target,可以返回事件的目标节点,也就是可以表示为当前的事件操作DOM,但是不是真正操作DOM。
如果希望事件到某个节点不在传播,可以使用 stopPropagation方法
// 事件传播到 p 元素后,就不再向下传播了
p.addEventListener('click', function (event) {
event.stopPropagation();
}, true);
// 事件冒泡到 p 元素后,就不再向上冒泡了
p.addEventListener('click', function (event) {
event.stopPropagation();
}, false);
上面代码中,p元素绑定了两个click事件的监听函数。stopPropagation方法只能阻止这个事件的传播,不能取消这个事件,因此,第二个监听函数会触发。输出结果会先是1,然后是2。如果想要彻底取消该事件,不再触发后面所有click的监听函数,可以使用stopImmediatePropagation方法。
Event对象
事件发生以后会产生一个事件对象,作为参数传给监听函数,浏览器原生提供一个 Event对象,所有的事件都是这个对象的实例,或者说继承了 Event.prototype对象。
Event本身就是一个构造函数,可以用来生成新的实例
enent = new Event(type,options);
Event接收两个参数,第一个参数type是字符串,表示事件的名称;第二个参数options是一个对象,表示事件对象的配置。该对象主要有下面两个属性。
- bubbles:布尔值,可选,默认为false,表示事件对象是否冒泡。
- cancelable:布尔值,可选,默认为false,表示事件是否可以被取消,即能否用Event.preventDefault()取消这个事件。一旦事件被取消,就好像从来没有发生过,不会触发浏览器对该事件的默认行为。
实例属性
- Event.bubbles属性返回一个布尔值,表示当前事件是否会冒泡
- Event.eventPhase属性返回一个整数常量,表示事件目前所处的阶段,0事件目前没有发生。1,事件目前处于捕获阶段,即处于从祖先节点向目标节点的传播过程中。2,事件到达目标节点,即Event.target属性指向的那个节点。3,事件处于冒泡阶段,即处于从目标节点向祖先节点的反向传播过程中。
- Event.cancelable属性返回一个布尔值,表示事件是否可以取消
事件发生以后,会经过捕获和冒泡两个阶段,依次通过多个 DOM 节点。因此,任意事件都有两个与事件相关的节点,一个是事件的原始触发节点(
Event.target),另一个是事件当前正在通过的节点(Event.currentTarget)。 - Event.type属性返回一个字符串,表示事件类型。 MDN文档地址:developer.mozilla.org/zh-CN/docs/…
鼠标事件/键盘事件
鼠标事件
继承了 MouseEvent接口
click点击dblclick双击mousedown按下鼠标mouseup松开鼠标mousemove鼠标在一个节点内移动,这个事件会连续触发,为避免性能问题,可以对该事件的监听函数做出一定限定,例如一定时间内只能运行一次mouseenter进入一个节点触发,进入子节点不会触发,只触发一次mouseover进入一个节点触发,进入子节点再次触发,只要在节点内部移动,事件会在子结点上多次触发mouseout离开节点触发,离开父节点触发mouseout离开触发,离开父节点不触发whell滚动滚轮
键盘事件
键盘事件由用户击打键盘触发,主要有keydown、keypress、keyup三个事件,它们都继承了KeyboardEvent接口。
keydown按下键盘触发keypress按下有值的键触发,按下有值的键时触发,即按下 Ctrl、Alt、Shift、Meta 这样无值的键,这个事件不会触发。对于有值的键,按下时先触发keydown事件,再触发这个事件。keyup松开键盘触发
表单事件
input事件当<input>、<select>、<textarea>的值发生变化时触发。对于复选框(<input type=checkbox>)或单选框(<input type=radio>),用户改变选项时,也会触发这个事件。input事件的一个特点,就是会连续触发,比如用户每按下一次按键,就会触发一次input事件。