JavaScript事件流理解与分析

事件流

事件流用简单的一句话描述就是:事件触发的顺序问题。可以先用一个简单的小例子直观的感受一下什么是事件流。

    <div id="wrapper">
        <div id="content"></div>
    </div>
    <script>
        var wrapperDom = document.getElementById("wrapper");
        var contentDom = document.getElementById("content");
        wrapperDom.addEventListener('click',function(){
            console.log("click wrapper");
        });
        contentDom.addEventListener('click',function(){
            console.log("click content");
        });
    </script>

然后我们点里面的id为content的Div,目前我们共同的认知是 wrapper和content的点击事件都会触发,但是问题就在于是先触发外层的还是里层的呢?这就是JS的事件流问题,也就是刚才说的事件触发顺序问题。下面附上运行结果(点击content)

Output:click content
        click wrapper

从运行的结果来看,content比wrapper先触发点击事件。

事件流历史

目前有两种事件流,一种是IE提出的冒泡流,还有一种是网景公司提出的捕获流,W3C统一后,JS同时支持这两种事件流,不过IE8及以下版本都只支持冒泡流,所以冒泡流更广为应用,为了更好地兼容低版本浏览器,建议使用冒泡流。

事件流分析

下图就对为什么上面的例子会先输出content后输出wrapper做了简单的演示:
事件流
我们可以根据这个图来分析事件流的整个过程,首先事件流被分为了3个阶段,分别是:捕获过程目标过程冒泡过程。事件流从window开始,最后再window结束。
①1~5:这是捕获过程,字面意思就是去捕获我们的触发事件的DOM元素,按照上面的例子,也就是找到我们点击的contentDom。
②5~6:这是目标过程,也就是找到目标元素后,在这个阶段触发事件,这里也就是触发了contentDom的点击事件。
③6~10:这是冒泡过程,简单理解就是从目标元素一层一层往上询问,你是否要触发事件?如果要,则触发,最终回溯到window为止。
通过上面的分析,我们也就能够明白为什么之前的例子,会先触发content再触发wrapper了。
但是! 并不是所有的情况都能按照我们想象的过程完美推进,这就要说到另外一个概念了,就是DOM Level,其实我们上面的例子是使用的DOM Level2,究竟什么是Dom Level?

DOM Lelvel

先用一个表格简单了解下有哪些DOM Level:

DOM Level捕获事件冒泡事件
DOM Level 0不支持支持
DOM Level 2支持支持
DOM Level 3支持支持

关于为什么没有DOM Level1?因为DOM Level1标准中并没有定义事件相关的内容,所以没有所谓的DOM Level1事件模型。
Dom Level 0Dom Level 2Dom Level 3最大的区别就在于Dom Level 0不支持在捕获阶段触发事件,仅支持在冒泡阶段触发。而Level2/3 都是同时支持两种的。

DOM Level 0

定义Level 0 的事件有两种形式,一种是直接写在html标签中,另一种是使用onclick这种方式:
方式1:

	<div id="wrapper" onclick="console.log(123);">

方式2:

    <script>
         var wrapperDom = document.getElementById("wrapper");
         wrapperDom.onclick = function(){
             console.log(123);
         }
    </script>

DOM Level 0 事件还有一个特点就是事件会被覆盖掉,什么意思呢,看下面的代码:

    <div id="wrapper">
        <div id="content"></div>
    </div>
    <script>
         var wrapperDom = document.getElementById("wrapper");
         wrapperDom.onclick = function(){
             console.log("click wrapper");
         }
         wrapperDom.onclick = function(){
             console.log("click wrapper again");
         }
    </script>

按照我们的想象,应该会将两个事件同时输出,但是事实上只输出了后绑定的事件:

Output:click wrapper again

所以,我们想要解除某个元素上绑定的事件,我们只需要这样做:

	wrapperDom.onclick = null;
DOM Level 2

DOM Level 2 既支持事件捕获也支持事件冒泡的,但是由于部分浏览器不支持事件捕获,所以还是尽量使用事件冒泡。DOM Level 2事件提供了两个方法供我们绑定和解绑:addEventListener()removeEventListener()。并且Level2支持一个元素绑定多个事件,看下面代码:

    <div id="wrapper">
        <div id="content"></div>
    </div>
    <script>
        var wrapperDom = document.getElementById("wrapper");
        var contentDom = document.getElementById("content");
        wrapperDom.addEventListener('click',function(){
            console.log("click wrapper");
        });
        contentDom.addEventListener('click',function(){
            console.log("click content");
        });
        contentDom.addEventListener('click',function(){
            console.log("click content again");
        });
    </script>

如上,我们给contentDom绑定了两个click事件,我们来看看输出结果:

Output:
click content
click content again
click wrapper

addEventListener()可以传入三个参数,分别是:事件名事件函数是否使用捕获事件
第三个参数是一个布尔值,true代表选择捕获阶段触发事件,false则代表冒泡触发。第三个参数缺省是false。下面的代码就是使用捕获阶段触发:

    <div id="wrapper">
        <div id="content"></div>
    </div>
    <script>
        var wrapperDom = document.getElementById("wrapper");
        var contentDom = document.getElementById("content");
        wrapperDom.addEventListener('click',function(){
            console.log("click wrapper");
        },true);
        contentDom.addEventListener('click',function(){
            console.log("click content");
        },true);
        contentDom.addEventListener('click',function(){
            console.log("click content again");
        },true);
    </script>

再来看看输出结果:

Output:
click wrapper
click content
click content again

看到输出结果我们便知,这些事件都是在捕获阶段就触发了。
然后,我们通过addEventListener()添加事件处理程序的程序只能使用removeEventListener()来移除。但是值得注意的是不能使用匿名函数来移除,否则是无效的:

        wrapperDom.addEventListener('click',function(){
            console.log("click wrapper");
        });
        wrapperDom.removeEventListener("click",function(){
            console.log("click wrapper");
        })

这样子是无法移除这个事件的,点击之后依然会触发之前绑定的事件,我们需要如下这么做:

        function handle(){
            console.log("click wrapper");
        }
        wrapperDom.addEventListener('click',handle);
        wrapperDom.removeEventListener("click",handle);

这么做就可以成功的移除我们绑定的事件了。
值得注意的是:DOM Level 2在IE中的绑定事件是attachEvent,解除绑定是detachEvent,在标准的浏览器绑定事件是addEventListener,解除绑定是removeEventListener

扩展

我们可以在控制台打印一个事件,这里我打印的是click的事件:
打印事件
对标红的的属性做一些说明:

属性名说明
bubbles事件是否冒泡
cancelable是否可以取消事件默认行为
currentTarget当前正在处理事件的元素
defaultPrevented是否已经调用了preventDefault()方法
detail事件相关的细节信息
eventPhase表示事件处理阶段,1捕获阶段 2目标阶段 3冒泡阶段
target事件的目标元素
type事件的类型
view与事件关联的抽象视图, 相当于发生事件的window对象

还有几个重要的函数

函数名说明
preventDefault()取消事件的默认行为
stopImmediatePropagation()取消事件的进一步捕获或者冒泡, 同时阻止任何事件处理程序被调用
stopPropagation()取消事件的进一步捕获或者冒泡
preventDefault()

preventDefault()函数是用于阻止原标签事件触发的,示例代码:

    <a href="http://www.baidu.com" id="tag">跳转百度</a>
    <script>
        var tagDom = document.getElementById("tag");
        tagDom.onclick = function(event) {
            console.log("click a tag");
            event.preventDefault();
        }
    </script>

如果我们不使用preventDefault()方法,就会同时跳转到百度并打印内容,但是如果加上了这个方法,就只会打印不会跳转,也就是阻止了标签本身的一些事件。
注意 在IE浏览器上面是event事件是没有preventDefault()这个属性的,所以在IE上,我们需要设置的属性是returnValue

	window.event.returnValue=false;
stopPropagation()

这个方法还是非常常用的,适用于阻止事件冒泡的,实例代码:

    <div id="wrapper">
        <div id="content"></div>
    </div>
    <script>
        var wrapperDom = document.getElementById("wrapper");
        var contentDom = document.getElementById("content");
        wrapperDom.onclick = function(){
            console.log("click wrapper");
        }
        contentDom.onclick = function(event){
            console.log("click content");
            event.stopPropagation();
        }
    </script>

如果我们不使用stopPropagation()方法,就会依次输出click content、click wrapper,但是如果在contentDom的点击事件加上这个方法,就可以阻止事件继续往上冒泡,也就不会冒泡到wrapperDom上了。因此只会执行contentDom对应的事件。
注意 在IE浏览器上面通过设置cancelBubble的值:

	event.cancelBubble=true;
事件委托

事件委托也叫事件代理,是一种前端性能优化的手段,其原理是利用了事件冒泡。举个简单的例子,我有下面这样一个DOM结构:

	<ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
        <li>6</li>
    </ul>

按照我们之前的做法,我们应该要获取每一个li的Dom,然后分别绑定上事件,但是这么做会有大量的DOM的操作,是非常消耗性能的。那么根据之前的说法,我只需要给ul绑定上事件,每次点击li然后事件会冒泡到ul上,再执行ul绑定的事件。这样就大大减少了DOM操作。并且除了DOM操作,事件的绑定也是非常消耗性能的。直接看例子:

    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
        <li>6</li>
    </ul>
    <script>
        var ulDom = document.getElementsByTagName("ul")[0];
      ulDom.onclick = function(e){
    var e = e || window.event;
    var target = e.target || e.srcElement;  //srcElement是IE的说法
    if(target.nodeName.toLowerCase() == 'li'){
        console.log(target.innerHTML);
    }
  }
    </script>

如果我们想给每个li执行不同的事件,我们可以给li加上id,通过id的判断去执行不同的事件。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值