javaScript 浏览器事件
1.事件基本概念
事件是指文档或者浏览器中发生的一些特定交互瞬间,比如打开某一个网页,浏览器加载完成后会触发load事件,当鼠标浮于某一个元素上时会触发hover事件,当鼠标点击某一个元素时会触发click事件等等。
事件处理就是当事件被触发吼,浏览器响应这个事件的行为,而这个行为所对应的代码即为事件处理程序。
2.事件操作:监听与移除监听
2.1监听事件
浏览器会根据一些事件作出相对应的事件处理,事件处理的前提是需要监听事件,监听事件的方法主要有以下三种:
2.1.1 HTML内联属性
即在HTML元素里直接填写与事件相关的属性,属性值为事件处理程序。示例如下:
<button onclick="console.log('You clicked me!');"></button>
onclick对应着click事件,所以当按钮被点击后,便会执行事件处理程序,即控制台输出“You clicked me!”。
不过我们需要指出的是,这种方式将HTML代码与JavaScript代码耦合在一起,不利于代码的维护,所以应该尽量避免使用这样的方式。
2.1.2 DOM属性绑定
通过直接设置某个DOM节点的属性来指定事件和事件处理程序,上代码
const btn = document.getElementById("btn");
btn.onclick = function(e) {
console.log("You clicked me!")
}
上面示例中,首先获得btn这个对象,通过给这个对象添加onclick
属性的方式来监听click
事件,这个属性值对应的就是事件处理程序。这段程序也被成为DOM 0级事件处理程序。
2.1.3 事件监听函数
标准的事件监听函数如下:
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
console.log("You click me!");
}, false);
上面的示例表示先获得表示节点的btn对象,然后在这个对象上面添加了一个事件监听器,当监听到click事件发生时,则调用回调函数,即在控制台输出“You clicked me!”
。addEventListener
函数包含了第三个参数false
,第三个参数的含义在后面的事件触发三个阶段之后再讲解。这段程序也被称作DOM 2级事件处理程序。IE9+、FireFox、Safari、Chrome 和 Opera 都是支持 DOM 2 级事件处理程序的,对于 IE8 及以下版本,则用 attacEvent() 函数绑定事件。
所以我们得写一段具有兼容性的代码:
function addEventHandler(obj, eventName, handler) {
if (document.addEventListener) {
obj.addEventListener(eventName, handler, false);
} else if (document.attachEvent) {
obj.attachEvent("on" + eventName, handler);
} else {
obj["on" + eventName] = handler;
}
}
2.2移除事件监听
在为某个元素绑定了一个事件后,如果想解除绑定,则需要removeEventListener
方法。
const handler = function() {
// hanlder logic
}
const btn = document.getElementById("btn");
btn.addEventListener("click", handler);
btn.removeEventListener("click", handler);
需要注意的是,绑定事件的回调函数不能是匿名函数,必须是一个已经被声明的函数,因为解除事件绑定时需要传递这个回调函数的引用。
同样,IE8 及以下版本也不支持上面的方法,而是用 detachEvent 代替。
const handler = function() {
// hnalder logic
}
const btn = document.getElementById("btn");
btn.attachEvent("onclick", handler);
btn.detachEvent("onclick", handler);
同样,可以写一段具有兼容性的删除事件函数:
function removeEventHandler(obj, eventName, handler) {
if (document.removeEventListener) {
obj.removeEventListener(eventName, handler, false);
} else if (document.detachEvent) {
obj.detachEvent("on" + eventName, handler);
} else {
obj["on" + eventName] = null;
}
}
DOM事件级别
DOM级别一共可以分为四个级别:DOM0级
、DOM1级
、DOM2级
、DOM3级
。而DOM事件分为3个级别:DOM 0
级事件处理,DOM2
级事件处理和DOM3
级事件处理。由于DOM1
级中没有事件的相关内容,所以没有DOM1
级事件。
DOM0级事件
el.onclick=function(){}
var btn = document.getElementById("btn");
btn.onclick = function() {
alert(this.innerHTML);
}
当希望为同一个元素、标签绑定多个同类型事件的时候(如给上面的这个btn元素绑定3个点击事件),是不背允许的。DOM0事件绑定,给元素的事件行为绑定方法,这些方法都是在当前元素事件行为的冒泡阶段(或者目标阶段)执行的。
DOM 2级事件
el.addEventListener(event-name,callback,useCapture)
- event-name: 事件名称,可以是标准的DOM事件。
- callback:回调函数,当事件触发时,函数会被注入一个参数为当前事件对象event。
- useCpature:默认是false,代表事件句柄在冒泡执行阶段执行(或者说注册的是冒泡事件),true表示事件句柄在捕获阶段执行(或者说注册的是捕获事件)
var btn = document.getElementById('btn');
btn.addEventListener("click", test, false);
function test(e) {
e = e || window.event;
alert((e.target || e.srcElement).innerHTML);
btn.removeEventListner("click", test);
}
IE9以下的IE浏览器不支持 addEventListener()和removeEventListener(),使用 attachEvent()与detachEvent() 代替,因为IE9以下是不支持事件捕获的,所以也没有第三个参数,第一个事件名称前要加on。可以对此做个兼容性处理。
DOM 3级事件
在DOM 2级事件的基础上添加了更多的事件类型。
- UI事件,当用户预页面的元素交互时触发,如:load、scroll
- 焦点事件,当元素获得或失去焦点时触发,如:blur、focus
- 鼠标事件,当用户通过谁啊哦在页面执行操作时触发如:dblclick、mouseup
- 滚轮事件,当使用鼠标滚轮或类似设备时触发,如:mousewheel
- 文本事件,当在文档中输入文本时触发,如:textinput
- 键盘事件,当用户通过键盘在页面上执行操作时触发,如:keydown、keypress
- 合成事件,当为IME(输入法编辑器)输入字符时触发,如:compositionstart
- 变动事件,当底层DOM结构发生变化时触发,如:DOMsubtreeModified。
- 同时DOM 3级事件也允许使用者自定义一些事件。
总结:
- DOM2级的好处是可以添加多个事件处理程序;DOM0级对每个事件支持持一个事件处理程序;
- 通过DOM2级添加的匿名函数无法移除,
addEventListener
和removeEventListener
的handler
必须同名 - 作用域:DOM 0的
handler
会在所属元素的作用域内运行,IE的handler
会在全局作用域运行,this === window
- 触发顺序:添加多个事件时,DOM2会按照添加顺序执行,IE会以相反的顺序执行
3.事件触发过程
事件流描述了页面接收事件的顺序。现代浏览器事件流包含三个过程,分别是捕获阶段、目标阶段和冒泡阶段。
下面详细地讲解这三个过程。
3.1捕获阶段
当我们对 DOM元素进行操作时,比如鼠标点击、悬浮等,就会有一个事件传输到这个DOM元素,这个事件从Window开始,依次经过document、html、body,再不断经过过子节点直到到达目标元素,从 Window到达目标元素父节点的过程称为捕获阶段,注意此时还未到达目标节点。
3.2目标阶段
捕获阶段结束时,事件到达了目标节点的父节点,最终到达目标节点,并在目标节点上触发了这个时间,这就是目标阶段。
需要注意的是,事件触发的目标节点为最底层的节点。比如下面这个例子:
<div>
<p>
你猜,目标在这里还是<span>哪里</span>
</p>
</div>
当我们点击“哪里“的时候,目标节点是<span></span>
,点击这里的时候,目标节点是<p></p>
,而当我们点击<p></p>
区域之外,<div></div>
区域之内,目标节点就是<div></div>
。
3.3 冒泡阶段
当事件到达目标节点之后,就会沿着原路返回,这个过程有点类似于水泡从水底浮出水面的过程,所以称这个过程为冒泡阶段。
现在再看addEventListener(eventName, handler, useCapture)
函数。第三个参数是useCapture
,代表是否在捕获阶段进行事件处理,如果是false,则在冒泡阶段进行事件处理,如果是true
则在捕获阶段进行处理,默认是false
。
冒泡事件的流程刚好是事件捕获的逆过程。我们来看个事件冒泡的例子:
<div id="outer">
<div id="inner">
</div>
</div>
<script>
window.onclick = function() {
console.log('window');
};
document.onclick = function() {
console.log('document');
};
document.documentElement.onclick = function() {
console.log('html');
};
document.body.onclick = function() {
console.log('body');
};
outer.onclick = function(ev) {
console.log('outer');
};
inner.onclick = function(ev) {
console.log('inner');
}
</script>
// 输出为 inner outter body html dcoument window
4.事件委托
JavaScript中,事件的委托表示给元素的父级或者祖级,甚至页面,由他们来绑定事件,然后利用事件冒泡的基本原理,通过事件目标对象进行检测,然后执行相关操作。
事件委托有两个优点:
- 减少内存消耗,提高性能
假设有一个列表,列表之中有大量的列表项,我们需要在点击每个列表项的时候响应一个事件。
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>
如果给每个列表项都一一绑定函数,那么对内存的消耗是非常大的,需要消耗更多性能。借助事件委托,我们只需要给父容器ul
绑定方法即可,这样不管点击哪一个后代元素,都会根据冒泡传播的传递机制,把容器的click行为处罚,然后把对应的方法执行,根据事件源,我们可以知道点击的是谁,从而完成不同的事。
- 动态绑定事件
在很多时候,我们需要通过用户操作动态的增删列表项元素,如果一开始给每个子元素绑定事件,那么在列表发生变化时,就需要从新给新增的元素绑定事件,给即将删去的元素解绑事件,如果用事件委托就会省去很多这样的麻烦。
接下来我们来实现上例中父元素#list下的li元素的事件委托到他的父层元素上:
// 给父层元素绑定事件
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);
}
});
5.事件对象
DOM0
和DOM2
的事件处理程序都会自动传入event
对象,即触发DOM
上的某个事件时,会产生一个事件对象,里面包含着所有和事件有关的信息。IE
中的event
对象取决于指定的事件处理程序的方法。
IE的
handler
会在全局作用域运行,this === window
所以在IE中会有window.event
、event
两种情况。
另外在IE
中,事件对象的属性也不一样,对应关系如下:
srcElement
=> target
returnValue
=> preventDefault()
cancelBubble
=> stopPropagation()
IE不支持事件捕获,因而只能取消事件冒泡,但stopPropagtion
可以同时取消事件捕获和冒泡。
只有在事件处理程序期间,
event
对象才会存在,一旦事件处理程序执行完成,event
对象就会被销毁。
1、event.preventDefault()
如果调用这个方法,默认事件行为将不在触发。什么是默认事件呢?例如表单 - 点击提交按钮跳转页面、a标签默认页面跳转或是锚点定位等。
很多时候我们使用a标签仅仅是想当做一个普通的按钮,点击实现一个功能,不想页面跳转,也不想锚点定位。
// 方法一
<a href="javascript:;">链接</a>
也可以通过JS方法来阻止,给其click事件绑定方法,当我们点击A标签的时候,先触发click事件,其次才会执行自己的默认行为
// 方法二
<a id="test" href="http://www.cnblogs.com">链接</a>
<script>
test.onclick = function(e) {
e = e || window.event;
return false;
}
</script>
// 方法三
<a id="test" href="http://www.cnblogs.com">链接</a>
<script>
test.onclick = function(e) {
e = e || window.event;
e.preventDefalut();
}
</script>
接下来我们看个例子:输入框最多只能输入六个字符,如何实现?
// 例子5
<input type="text" id="tempInp">
<script>
tempInp.onkeydown = function(ev) {
ev = ev || window.event;
let val = this.value.trim();
let len = val.lenght;
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>
2.event.stopPropagation()
& event.stopImmediatePropagation()
event.stopPropagation()
方法阻止事件冒泡到父元素,阻止任何父事件处理程序被执行(一般我们认为stopPropagation是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件)。上面提到事件冒泡阶段是指事件从目标节点紫霞而上的向window
对象传播的阶段。我们在上面的例子中的inner
元素click
事件上,添加event.stopPropagation()
这句话后,就阻止了父事件的执行,最后只打印了'inner'
。
inner.onclick = function(ev) {
console.log('inner');
ev.stopPropagation();
}
stopImmediatePropagation 既能阻止事件向父元素冒泡,也能阻止元素同事件类型的其它监听器被触发。而 stopPropagation 只能实现前者的效果。我们来看个例子:
<body>
<button id="btn">click me to stop propagation</button>
</body>
<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');
});
// btn click 1
</script>
如上所示,使用stopImmediatePropagation
后,点击按钮时,不仅body
绑定事件不会触发,与此同时按钮的另一个点击事件也不触发。
event.target & event.currentTarget
event.target
指向引起触发事件的元素,而event.currentTarget
则是事件绑定的元素,只有被点击的那个目标元素的event.target
才会等于event.currentTarget
。也就是说,event.currentTarget始终是监听事件这,而event.target是事件的真正发出者。
6.捕获与冒泡的顺序问题
当有很多层交互嵌套时,事件捕获和时间冒泡的先后顺序看起来是不好确定的。下面分5种情况讨论给它们的顺序,以及如何规避意外情况的发生。
1、在外层div注册事件,点击内层div来触发事件时,捕获事件总是要比冒泡事件先触发(与代码顺序无关)
假设,有这样的html结构:
<div id="test" class="test">
<div id="testInner" class="test-inner">
</div>
</div>
然后,我们在外层div上注册两个click事件,分别是捕获事件和冒泡事件,代码如下:
const btn = document.getElementById("test");
// 捕获事件
btn.addEventListener("click", function(e) {
alert("capture is ok");
}, true);
// 冒泡事件
btn.addEventListener("click", function(e) {
alert("bubble is ok");
}, false)
点击内层的div,先弹出capture is ok,后弹出bubble is ok。只有当真正触发事件的 DOM元素是内层时,外层DOM元素才有机会模拟捕获事件和冒泡事件。
2、当在触发事件的DOM元素上注册事件时,那个先注册就先执行那个
html 结构同上,js代码如下:
const btnInner = document.getElementById("testInner");
// 冒泡事件
btnInner.addEventListener("click", function(e) {
alert("bubble is ok");
}, false);
// 捕获事件
btnInner.addEventListener("click", function(e) {
alert("caapture is ok");
}, true);
在本例中,冒泡事件先注册,所以先执行。所以,点击内层div,先弹出bubble is ok
,再弹出caputre is ok
。
3、当外层div和内层div同时注册了捕获事件时,点击内层的div时,外层div的事件一定会先触发
const btn = document.getElementById("test");
const btnInner = document.getElementById("testInner");
btnInner.addEventListener("click", function(e) {
alert("inner capture is ok");
}, true);
btn.addEventListener("click", function(e) {
alert("outer capture is ok");
}, true)
虽然外层 div 的事件注册在后面,但会先触发。所以,结果是先弹出 outer capture is ok
,再弹出 inner capture is ok
。
4、同理,当外层div和内层div都同时注册了冒泡事件,点击内层div时,一定是内层div事件先触发。
const btn = document.getElementById("test");
const btnInner = document.getElementById("testInner");
btn.addEventLisntener("click", function(e) {
alert("outer bubble is ok");
}, false);
btnInner.addEventListener("click", function(e) {
alert("inner bubble is ok");
}, false)
先弹出inner bubble is ok
,再弹出outer bubble is ok
。
5、阻止事件的派发
通常情况下,我们都希望点击某个div
时,就触发自己的事件回调。比如,明明点击的是内层div
,但是外层div
的事件也触发了,这就不是我们想要的了。这时,就需要阻止事件的派发。
事件触发时,会默认传入一个event
对象,这个event
对象上有一个方法:stopPropagation
。MDN上的解释是:阻止捕获和冒泡阶段中,当前事件的进一步传播。所以通过此方法,让外层div
接收不到事件,自然也就不会触发了。
btnInner.addEventListener("click", function(e) {
// 阻止冒泡
e.stopPropagation();
alert(”inner bubble is ok“);
}, false);