理解DOM中的事件流的概念


    什么是事件流

    :用术语说流是对输入输出设备的抽象。以程序的角度说,流是具有方向的数据。

    事件流:从页面中接收事件的顺序。也就是说当一个事件产生时,这个事件的传播过程,就是事件流。

    事件:用户或者浏览器自身执行的某个动作,比如load,click,mousemove等

    事件处理程序:相应处理某个事件的函数叫做事件处理函数(也叫做事件侦听器)

    比如说React中的单向数据流,Node中的流,又或是今天本文所讲的DOM事件流。都是流的一种生动体现。


    理解DOM中的事件流

    当浏览器发展到第四代时(IE4和Netscape Communicator 4),浏览器团队遇到一个很有意思的问题:页面的哪一部分会拥有特定的事件?想象下在一张纸上有一组同心圆,如果你把手指放在圆心上,那么你的手指指向的不是一个圆,而是一组圆。两家公司的开发团队在看待浏览器事件方面还是一致的。如果你单击了某个按钮,那么同时你也单击了按钮的容器元素,甚至整个页面。
    事件流描述的是从页面中接受事件的顺序。但有意思的是,IE和Netscape开发团队居然提出了两个截然相反的事件流概念。IE的事件流是事件冒泡流,而Netscape的事件流是事件捕获流。


    IE提出的事件冒泡

    事件冒泡即事件开始时,由最具体的元素接收(也就是事件发生所在的节点),然后逐级传播到较为不具体的节点。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Document</title>
    </head>
    <body>
    <button id="click">点击</button>
    <script>
    (function(){
    var btn = document.getElementById("click");
    btn.onclick = function(){
    console.log("1. button");
    }
    document.body.onclick = function(){
    console.log("2. document.body");
    }
    document.onclick = function(){
    console.log("3. document");
    }
    window.onclick = function(){
    console.log("4. window");
    }
    })()
    </script>
    </body>
    </html>

    在代码所示的页面中,如果点击了button,那么这个点击事件会得到如下的结果:
    代码结果截图
    也就是说,click事件首先在button元素上发生,然后逐级向上传播。这就是事件冒泡。


    netscape提出的事件捕获

    事件捕获的概念,与事件冒泡正好相反。它认为当某个事件发生时,父元素应该更早接收到事件,具体元素则最后接收到事件。比如说刚才的demo,如果是事件捕获的话,事件发生顺序会是刚好与上面相反的。即window,document,document.body,button。

    虽然事件捕获是Netscape唯一支持的事件流模型,但IE9、Safari、Chrome、Opera和Firefox目前也都支持这种事件流模型。但由于老版本的浏览器不支持,因此很少有人使用事件捕获。

    所以放心的使用事件冒泡,有特殊需要再使用事件捕获即可。


    DOM事件流

    DOM事件流可以分为下面3个阶段:

    1. 事件捕获阶段
    2. 处于目标阶段
    3. 事件冒泡阶段
      dom事件

    事件捕获阶段

    也就是说,当事件发生时,首先发生的是事件捕获,为父元素截获事件提供了机会。
    例如,我把上面的Demo中,window点击事件更改为使用事件捕获模式。

    addEventListener最后一个参数,为true则代表使用事件捕获模式,false则表示使用事件冒泡模式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <script>
    (function(){
    var btn = document.getElementById("click");
    btn.addEventListener("click",function(){
    console.log("1. button");
    },true)
    //省略document.body和document
    .....
    window.addEventListener("click",function(){
    console.log("4. window");
    },true)
    })()
    </script>

    结果如下:

    可以看到,点击事件先被父元素截获了,且该函数只在事件捕获阶段起作用。

    在DOM事件流中,事件的目标在捕获阶段不会接受到事件。这意味着在捕获阶段,事件从document到body后就定停止了。下一个阶段是处于目标阶段,于是事件在button上发生,并在事件处理中被看成冒泡阶段的一部分。然后,冒泡阶段发生,事件又传播回document。

    但是:我们的各大浏览器总是不喜欢按照规范来,IE9,Safari,chrome,firefox及其更高的版本中都会在捕获阶段出发事件对象上的事件,最后导致有两个机会在目标对象上操作事件。


    处于目标与事件冒泡阶段

    事件到了具体元素时,在具体元素上发生,并且被看成冒泡阶段的一部分。
    随后,冒泡阶段发生,事件开始冒泡。


    阻止事件冒泡

    件冒泡过程,是可以被阻止的。防止事件冒泡而带来不必要的错误和困扰。

    这个方法就是:stopPropagation()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    (function(){
    var btn = document.getElementById("click");
    btn.addEventListener("click",function(event){
    console.log("1. button");
    event.stopPropagation();
    console.log('Stop Propagation!');
    },false)
    //省略document.body和document
    .....
    window.addEventListener("click",function(){
    console.log("4. window");
    },false)
    })()

    最后结果是:1.button,Stop Propagation!。通过stopPropagation();阻止了事件的冒泡。


    事件处理程序类别

    刚刚我们已经讲了事件处理程序就是相应处理某个事假的函数。它可以分为几个类别:

    html事件处理程序

    某个元素支持的某个事件可以用与事件处理程序同名的html特性来指定,该特性的值是能够执行的javascript代码,这也是我们最初学js,最开始的方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <script>
    function show(){
    alert('我被点击了');
    }
    /*
    点击后也会弹出 '我被点击了'
    */
    </script>
    <input type="button" value="点击" onclick="show()" />

    优点:简单明了,省去获取元素等一系列前提操作

    缺点:html代码与js代码高度耦合,不符合分离原则


    DOM0级别事件处理函数

    DOM0级别事件处理函数,使用 element.on[eventname]=fn的方式给元素添加事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <input type="button" value="点击" id="click" />
    <script>
    var oBtn=document.getElementById('click');
    //该方式被认为是元素的方法,即事件处理程序在元素的作用域中进行,this即该元素本身
    oBtn.onclick=function(){
    alert(this.id);//click
    }
    //注意:删除该事件处理程序可以用如下方法
    oBtn.onclick=null;//即点击后不再有任何反应
    </script>


    DOM2级事件处理程序

    DOM2级添加了addEventListener(添加事件处理程序)和removeEventListener(移除事件处理程序),也就是我们刚刚讲的上面的DOM2例子。

    添加事件处理函数addEventListener

    参数1 指定事件名称...click mouseover mouseout
    参数2 事件处理程序(匿名函数或者有名函数)
    参数3 true(捕获阶段发生) or false(冒泡阶段发生)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <input type="button" value="点击" id="click" />
    <script>
    var oBtn=document.getElementById('click');
    oBtn.addEventListener('click',function(){
    alert(this.id)//click this指的是该元素作用域内
    },false)
    //注意该种方式可以给一个函数添加多个事件处理函数,执行顺序与添加顺序相同
    oBtn.addEventListener('click',function(){
    alert('Hello World')//click
    },false)
    </script>

    移除事件处理函数removeEventListener

    如果事件处理函数是有名函数,则可以通过名字来移除,匿名函数无法移除。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <input type="button" value="点击" id="click" />
    <script>
    var oBtn=document.getElementById('click');
    function showId(){
    alert(this.id);
    };
    function HelloWorld(){
    alert('HellowWorld');
    }
    oBtn.addEventListener("click",showId,false);
    oBtn.addEventListener("click",HelloWorld,false);
    oBtn.removeEventListener('click',showId,false)
    </script>

    最后只能弹出HellowWorld

    IE事件处理程序attachEvent,detachEvent

    ie实现了与dom类似的两个方法,attachEvent(添加),detachEvent(删除)

    1
    2
    3
    oBtn.attachEvent('onclick',showId);//这时候会报错,因为这里的是在window的作用域内
    //修改如下
    oBtn.detachEvent('onclick',showId) ;//点击没有任何反应