这是前端面试题系列的第 7 篇,你可能错过了前面的篇章,能够在这里找到:html
最近,小伙伴L 在温习 《JavaScript高级程序设计》中的 事件
这一章节时,产生了困惑。前端
他问了我这样几个问题:vue
.
的形式 连结在事件以后,就能够监听原生事件了。它的背后有什么原理?浏览器中的事件机制,也常常在面试中被说起。因此这回,咱们共同探讨了这些问题,并最终整理成文,但愿帮到有须要的同窗。node
先从概念提及,DOM 事件流分为三个阶段:捕获阶段
、目标阶段
、冒泡阶段
。先调用捕获阶段的处理函数,其次调用目标阶段的处理函数,最后调用冒泡阶段的处理函数。react
网景公司提出了 事件捕获
的事件流。这就比如采矿的小游戏,每次都会从地面开始一路往下,抛出抓斗,捕获矿石。在上图中就是,某个 div 元素触发了某个事件,最早获得通知的是 window,而后是 document,依次往下,直到真正触发事件的那个目标元素 div 为止。jquery
而 事件冒泡
则是由微软提出的,与之顺序相反。仍是刚才的采矿小游戏,命中目标后,抓斗再沿路收回,直到冒出地面。在上图中就是,事件会从目标元素 div 开始依次往上,直到 window 对象为止。面试
w3c 为了制定统一的标准,采起了折中的方式:先捕获在冒泡
。同一个 DOM 元素能够注册多个同类型的事件,经过 addEventListener 和 removeEventListener 进行管理。addEventListener 的第三个参数,就是为了捕获和冒泡准备的。element-ui
注册事件
(addEventListener) 有三个参数,分别为:"事件名称", "事件回调", "捕获/冒泡"(布尔型,true表明捕获事件,false表明冒泡事件)。浏览器
target.addEventListener(type, listener[, useCapture]);
复制代码
解除事件
(removeEventListener) 也有三个参数,分别为:"事件名称", "事件回调", "捕获/冒泡"(Boolean 值,这个必须和注册事件时的类型一致)。bash
target.removeEventListener(type, listener[, useCapture]);
复制代码
要想注册过的事件可以被解除,必须将回调函数保存起来,不然没法解除。例如这样:
const btn = document.getElementById("test");
//将回调存储在变量中
const fn = function(e){
alert("ok");
};
//绑定
btn.addEventListener("click", fn, false);
//解除
btn.removeEventListener("click", fn, false);
复制代码
当有多层交互嵌套时,事件捕获和冒泡的前后顺序,彷佛不是那么好理解。接下来,将分 5 种状况讨论它们的顺序,以及如何规避意外状况的发生。
假设,有这样的 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 元素才有机会模拟捕获事件和冒泡事件。
html 结构同上,js 代码以下:
const btnInner = document.getElementById("testInner");
//冒泡事件
btnInner.addEventListener("click", function(e){
alert("bubble is ok");
}, false);
//捕获事件
btnInner.addEventListener("click", function(e){
alert("capture is ok");
}, true);
复制代码
本例中,冒泡事件先注册,因此先执行。因此,点击内层 div,先弹出 bubble is ok
,再弹出 capture is ok
。
js 代码以下:
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
。
const btn = document.getElementById("test");
const btnInner = document.getElementById("testInner");
btn.addEventListener("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
。
一般状况下,咱们都但愿点击某个 div 时,就只触发本身的事件回调。好比,明明点击的是内层 div,可是外层 div 的事件也触发了,这是就不是咱们想要的了。这时,就须要阻止事件的派发。
事件触发时,会默认传入一个 event 对象,这个 event 对象上有一个方法:stopPropagation
。MDN 上的解释是:阻止 捕获 和 冒泡 阶段中,当前事件的进一步传播。因此,经过此方法,让外层 div 接收不到事件,天然也就不会触发了。
btnInner.addEventListener("click", function(e){
//阻止冒泡
e.stopPropagation();
alert("inner bubble is ok");
}, false);
复制代码
咱们常常会遇到,要监听列表中多项 li 的状况,假设咱们有一个列表以下:
<ul id="list">
<li id="item1">item1</li>
<li id="item2">item2</li>
<li id="item3">item3</li>
<li id="item4">item4</li>
</ul>
复制代码
若是咱们要实现如下功能:当鼠标点击某一 li 时,输出该 li 的内容,咱们一般的写法是这样的:
window.onload=function(){
const ulNode = document.getElementById("list");
const liNodes = ulNode.children;
for(var i=0; i<liNodes.length; i++){
liNodes[i].addEventListener('click',function(e){
console.log(e.target.innerHTML);
}, false);
}
}
复制代码
在传统的事件处理中,咱们可能会按照须要,为每个元素添加或者删除事件处理器。然而,事件处理器将有可能致使内存泄露,或者性能降低,用得越多这种风险就越大。JavaScript 的事件代理,则是一种简单的技巧。
事件代理,用到了在 JavaSciprt 事件中的两个特性:事件冒泡 和 目标元素。使用事件代理,咱们能够把事件处理器添加到一个元素上,等待一个事件从它的子级元素里冒泡上来,而且能够得知这个事件是从哪一个元素开始的。
改进后的 js 代码以下:
window.onload=function(){
const ulNode=document.getElementById("list");
ulNode.addEventListener('click', function(e) {
/*判断目标事件是否为li*/
if(e.target && e.target.nodeName.toUpperCase()=="LI"){
console.log(e.target.innerHTML);
}
}, false);
};
复制代码
回到文章开头的问题:了解事件流的顺序,对平常的工做有什么帮助呢?我总结了如下几个注意点。
好比 href 的连接跳转,submit 的表单提交等。能够在方法的最后,加上一行 return false;
。它会阻止经过 on 的方式绑定的事件的默认事件。
ele.onclick = function() {
……
// 经过返回 false 值,阻止默认事件行为
return false;
}
复制代码
另外,重写 onclick 会覆盖以前的属性,因此解绑事件能够这么写:
// 解绑事件,将 onlick 属性设为 null 便可
ele.onclick = null;
复制代码
前面说过 stopPropagation 的定义是:终止事件在传播过程的捕获、目标处理或起泡阶段进一步传播。事件再也不被分派到其余节点上。
// 事件捕获到 ele 元素后,就再也不向下传播了
ele.addEventListener('click', function (event) {
event.stopPropagation();
}, true);
// 事件冒泡到 ele 元素后,就再也不向上传播了
ele.addEventListener('click', function (event) {
event.stopPropagation();
}, false);
复制代码
可是,stopPropagation 只会阻止当前元素 同类型的
事件冒泡或捕获的传播,并不会阻止该元素上 其余类型
事件的监听。以 click 事件为例:
ele.addEventListener('click', function (event) {
event.stopPropagation();
console.log(1);
});
ele.addEventListener('click', function(event) {
// 仍然能够触发
console.log(2);
});
复制代码
若是想禁用以后全部的 click 事件,就要用到 stopImmediatePropagation 了。可是,须要注意的是,stopImmediatePropagation 只会禁用以后注册的同类型的监听事件。就好比阻止了以后的 click 事件监听函数,但别的事件类型如 mousedown、dblclick 之类,仍是能够监听到的。
ele.addEventListener('click', function (event) {
event.stopImmediatePropagation();
console.log(1);
});
ele.addEventListener('click', function(event) {
// 不会触发
console.log(2);
});
ele.addEventListener('mousedown', function(event) {
// 会触发
console.log(3);
});
复制代码
jquery 中的 on 是事件冒泡。当用 return false; 阻止浏览器的默认行为时,会作下面这 3 件事:
这 3 件事中,只有 preventDefault 是用来阻止默认行为的。除非你还想阻止事件冒泡,不然直接用 return false; 会埋下隐患。
angular 是个一应俱全的框架,彷佛学完它的一整套以后,就能玩转世界了。它加工封装了许多原生的东西,其中就包括了 event,只是前面须要加一个 $,表示这是 angular 中的特有对象。
// template
<div>
<button (click)="doSomething($event)">Click me</button>
</div>
// js
doSomething($event: Event) {
$event.stopPropagation();
...
}
复制代码
event 当作原生的事件对象来用了。
在 vue 的自定义组件中绑定原生事件,须要用到修饰符 native。
那是由于,咱们的自定义组件,最终会渲染成原生的 html 标签,而非相似于 这样的自定义组件。若是想让一个普通的 html 标签触发事件,那就须要对它作事件监听(addEventListener)。修饰符 native 的做用就在这里,它能够在背后帮咱们绑定了原生事件,进行监听。
一个经常使用的场景是,配合 element-ui 作登陆界面时,输完帐号密码,想按一下回车就能登陆。就能够像下面这样用修饰符:
<el-input
class="input"
v-model="password" type="password"
@keyup.enter.native="handleSubmit">
</el-input>
复制代码
el-input 就是自定义组件,而 keyup 就是原生事件,须要用 native 修饰符进行绑定才能监听到。
想要在 react 的事件回调中使用 event 对象,会产生困扰,会发现很多原生的属性都是 null。
那是由于在 react 中的事件,实际上是合成事件(SyntheticEvent),并非浏览器的原生事件,但它也符合 w3c 规范。
举一个简单的例子,咱们要实现一个组件,它有一个按钮,点击按钮后会显示一张图片,点击这张图片以外的任意区域,能够隐藏这张图片,可是点击该图片自己时,不会隐藏。代码以下:
class ShowImg extends Component {
constructor(props) {
super(props);
this.state = {
active: false
};
}
componentDidMount() {
document.addEventListener('click', this.hideImg.bind(this));
}
componentWillUnmount() {
document.removeEventListener('click', this.hideImg);
}
hideImg () {
this.setState({ active: false });
}
handleClickBtn() {
this.setState({ active: !this.state.active });
}
handleClickImg (e) {
e.stopPropagation();
}
render() {
return (
<div className="img-wrapper">
<button
className="showImgBtn"
onClick={this.handleClickBtn.bind(this)}>
显示图片
</button>
<div
className="img"
style={{ display: this.state.active ? 'block' : 'none' }}
onClick={this.handleClickImg.bind(this)}>
<img src="@/assets/avatar.jpg" >
</div>
</div>
);
}
}
复制代码
按照以前说的原生事件机制,咱们会错误地认为经过:
handleClickImg (e) {
e.stopPropagation();
}
复制代码
就能够阻止事件的派发了,但其实无法这么作。想要解决这个问题,固然也不复杂,就把 react 的事件和原生事件分开便可。
componentDidMount() {
document.addEventListener('click', this.hideImg.bind(this));
document.addEventListener('click', this.imgStopPropagation.bind(this));
}
componentWillUnmount() {
document.removeEventListener('click', this.hideImg);
document.removeEventListener('click', this.imgStopPropagation);
}
hideImg () {
this.setState({ active: false });
}
imgStopPropagation (e) {
e.stopPropagation();
}
复制代码
当对一个元素进行事件监听的时候,它的回调函数里就会默认传递一个参数 event,它是一个对象,包含了许多属性。我列出了一些比较经常使用的属性:
事件机制在浏览器中很是有用,全部用户的交互型操做,都依赖于它。现代 JavaScript 框架应用中,咱们也都离不开与原生事件的交互。
因此,在理解了事件流的概念,清楚了事件捕获与冒泡的顺序,掌握了一些原生事件的技巧以后,相信下次再遇到坑的时候,能够少走一些弯路了。
PS:欢迎关注个人公众号 “超哥前端小栈”,交流更多的想法与技术。