媒介
这是前端口试题系列的第 7 篇,你可以错过了前面的篇章,可以在这里找到:
近来,小伙伴L 在复习 《JavaScript高等程序设计》中的 事宜
这一章节时,发生了疑心。
他问了我如许几个题目:
- 相识事宜流的递次,对一样平常的事情有什么协助么?
- 在 vue 的文档中,有一个修饰符 native ,把它用
.
的情势 连结在事宜以后,就可以监听原生事宜了。它的背地有什么道理? - 事宜的 event 对象中,有很多的属性和要领,该怎样运用?
浏览器中的事宜机制,也经常在口试中被提及。所以这回,我们合营探讨了这些题目,并终究整顿成文,愿望帮到有须要的同砚。
事宜流的观点
先从观点提及,DOM 事宜流分为三个阶段:捕捉阶段
、目的阶段
、冒泡阶段
。先挪用捕捉阶段的处置惩罚函数,其次挪用目的阶段的处置惩罚函数,末了挪用冒泡阶段的处置惩罚函数。
网景公司提出了 事宜捕捉
的事宜流。这就比方采矿的小游戏,每次都邑从地面最早一起往下,抛出抓斗,捕捉矿石。在上图中就是,某个 div 元素触发了某个事宜,最早获得关照的是 window,然后是 document,顺次往下,直到真正触发事宜的谁人目的元素 div 为止。
而 事宜冒泡
则是由微软提出的,与之递次相反。照样适才的采矿小游戏,掷中目的后,抓斗再沿路收回,直到冒出地面。在上图中就是,事宜会从目的元素 div 最早顺次往上,直到 window 对象为止。
w3c 为了制订一致的范例,采取了折衷的体式格局:先捕捉在冒泡
。同一个 DOM 元素可以注册多个同范例的事宜,经由历程 addEventListener 和 removeEventListener 举行治理。addEventListener 的第三个参数,就是为了捕捉和冒泡预备的。
注册事宜
(addEventListener) 有三个参数,分别为:”事宜称号”, “事宜回调”, “捕捉/冒泡”(布尔型,true代表捕捉事宜,false代表冒泡事宜)。
target.addEventListener(type, listener[, useCapture]);
- type 示意事宜范例的字符串。
- listener 是一个完成了 EventListener 接口的对象,或许是一个函数。当所监听的事宜范例触发时,会吸收到一个事宜关照对象(完成了 Event 接口的对象)。
- capture 示意 listener 会在该范例的事宜捕捉阶段,流传到该 EventTarget 时触发,它是一个 Boolean 值。
消除事宜
(removeEventListener) 也有三个参数,分别为:”事宜称号”, “事宜回调”, “捕捉/冒泡”(Boolean 值,这个必需和注册事宜时的范例一致)。
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个注重点
当有多层交互嵌套时,事宜捕捉和冒泡的先后递次,好像不是那末好明白。接下来,将分 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("capture is ok");
}, true);
本例中,冒泡事宜先注册,所以先实行。所以,点击内层 div,先弹出 bubble is ok
,再弹出 capture is ok
。
3.当外层 div 和内层 div 同时注册了捕捉事宜时,点击内层 div 时,外层 div 的事宜一定会先触发
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
。
4.同理,当外层 div 和内层 div 都同时注册了冒泡事宜,点击内层 div 时,一定是内层 div 事宜先触发。
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
。
5.阻挠事宜的派发
一般状况下,我们都愿望点击某个 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);
};
一些经常使用技能
回到文章开首的题目:相识事宜流的递次,对一样平常的事情有什么协助呢?我总结了以下几个注重点。
1. 阻挠默许事宜
比方 href 的链接跳转,submit 的表单提交等。可以在要领的末了,加上一行 return false;
。它会阻挠经由历程 on 的体式格局绑定的事宜的默许事宜。
ele.onclick = function() {
……
// 经由历程返回 false 值,阻挠默许事宜行动
return false;
}
别的,重写 onclick 会掩盖之前的属性,所以解绑事宜可以这么写:
// 解绑事宜,将 onlick 属性设为 null 即可
ele.onclick = null;
2. stopPropagation 和 stopImmediatePropagation
前面说过 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);
});
3. jquery 中的 return false;
jquery 中的 on 是事宜冒泡。当用 return false; 阻挠浏览器的默许行动时,会做下面这 3 件事:
- event.preventDefault();
- event.stopPropagation();
- 住手回调函数实行并马上返回。
这 3 件事中,只要 preventDefault 是用来阻挠默许行动的。除非你还想阻挠事宜冒泡,不然直接用 return false; 会埋下隐患。
4. angular 中的 $event
angular 是个应有尽有的框架,好像学完它的一整套以后,就可以玩转天下了。它加工封装了很多原生的东西,其中就包含了 event,只是前面须要加一个 $,示意这是 angular 中的特有对象。
// template
<div>
<button (click)="doSomething($event)">Click me</button>
</div>
// js
doSomething($event: Event) {
$event.stopPropagation();
...
}
$event 在这里作为一个变量,显式地
传入回调函数,以后就可以将 $event 当作原生的事宜对象来用了。
5. vue 中的 native 修饰符
在 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 修饰符举行绑定才监听到。
6. react 中的合成事宜
想要在 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();
}
7. 事宜对象 event
当对一个元素举行事宜监听的时刻,它的回调函数里就会默许通报一个参数 event,它是一个对象,包含了很多属性。我列出了一些比较经常使用的属性:
- event.target:指的是触发事宜的谁人节点,也就是事宜最初发作的节点。
- event.target.matches:可以对症结节点举行婚配,来实行响应操纵。
- event.currentTarget:指的是正在实行的监听函数的谁人节点。
- event.isTrusted:示意事宜是不是是实在用户触发。
- event.preventDefault():作废事宜的默许行动。
- event.stopPropagation():阻挠事宜的派发(包含了捕捉和冒泡)。
- event.stopImmediatePropagation():阻挠同一个事宜的其他监听函数被挪用。
总结
事宜机制在浏览器中非常有效,一切用户的交互型操纵,都依赖于它。当代 JavaScript 框架运用中,我们也都离不开与原生事宜的交互。
所以,在明白了事宜流的观点,清晰了事宜捕捉与冒泡的递次,控制了一些原生事宜的技能以后,置信下次再碰到坑的时刻,可以少走一些弯路了。
PS:迎接关注我的民众号 “超哥前端小栈”,交换更多的主意与手艺。