「前端面试题系列7」Javascript 中的事宜机制(从原生到框架)

《「前端面试题系列7」Javascript 中的事宜机制(从原生到框架)》

媒介

这是前端口试题系列的第 7 篇,你可以错过了前面的篇章,可以在这里找到:

近来,小伙伴L 在复习 《JavaScript高等程序设计》中的 事宜 这一章节时,发生了疑心。

他问了我如许几个题目:

  • 相识事宜流的递次,对一样平常的事情有什么协助么?
  • 在 vue 的文档中,有一个修饰符 native ,把它用 . 的情势 连结在事宜以后,就可以监听原生事宜了。它的背地有什么道理?
  • 事宜的 event 对象中,有很多的属性和要领,该怎样运用?

浏览器中的事宜机制,也经常在口试中被提及。所以这回,我们合营探讨了这些题目,并终究整顿成文,愿望帮到有须要的同砚。

事宜流的观点

先从观点提及,DOM 事宜流分为三个阶段:捕捉阶段目的阶段冒泡阶段。先挪用捕捉阶段的处置惩罚函数,其次挪用目的阶段的处置惩罚函数,末了挪用冒泡阶段的处置惩罚函数。

《「前端面试题系列7」Javascript 中的事宜机制(从原生到框架)》

网景公司提出了 事宜捕捉 的事宜流。这就比方采矿的小游戏,每次都邑从地面最早一起往下,抛出抓斗,捕捉矿石。在上图中就是,某个 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:迎接关注我的民众号 “超哥前端小栈”,交换更多的主意与手艺。

《「前端面试题系列7」Javascript 中的事宜机制(从原生到框架)》

    原文作者:micherwa
    原文地址: https://segmentfault.com/a/1190000018266823
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞