【前端盲點】事件的幾個階段你真的瞭解麼???

前言

前端時間我寫過幾篇關於事件的博客:javascript

結果被團隊的狗蛋讀了,發現其中一塊」特別「的地方,而後以後讀了Barret Lee 的一篇博客:[解惑]JavaScript事件機制css

發現該博主以前對這個問題可能也有必定」誤解「html

以後再陸陸續續問了團隊中幾個高手,狗蛋的問題都得不到解釋,並且不少比較資深的前端對這個問題的認識也是有問題的前端

因此,這裏就拿出來講說,各位看看就好java

事件階段

引用:node

羣裏童鞋問到關於事件傳播的一個問題:「事件捕獲的時候,阻止冒泡,事件到達目標以後,還會冒泡嗎?」。c++

初學 JS 的童鞋常常會有諸多疑問,我在不少 QQ 羣也混了好幾年了,耳濡目染也也收穫了很多,之後會總結下問題的結論,順便說說相關知識的擴展~chrome

若是貿然回答還會冒泡,這不太好的,稍微嚴謹點考慮 0級 DOM 事件模型的話,這個答案是否認的。可是在 2級 DOM 事件模型中,答案是確定的,這個問題值得探討記錄下。api

本文地址:http://www.cnblogs.com/hustskyking/p/problem-javascript-event.html 數組

三個階段

事件冒泡即由最具體的元素(文檔嵌套最深節點)接收,而後逐步上傳至document

事件捕獲會由最早接收到事件的元素而後傳向最裏邊(咱們能夠將元素想象成一個盒子裝一個盒子,而不是一個積木堆積)

這句話其實解釋的很清楚了,事件的觸發點應該是最早接收到事件的元素或者最具體的元素

DOM2級事件規定事件包括三個階段:

① 事件捕獲階段

② 處於目標階段

③ 事件冒泡階段

一切都很美好,好比如下代碼不少人會自覺得是的認爲先執行捕獲,再執行冒泡:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <style type="text/css">
         #p { width: 300px; height: 300px; padding: 10px;  border: 1px solid black; }
         #c { width: 200px; height: 200px; border: 1px solid red; }
         #sub { width: 100px; height: 100px; border: 1px solid red; }
    </style>
</head>
<body>
    <div id="p">
        parent
    </div>
    <script type="text/javascript">
      window.alert = function (msg) {
        console.log(msg);
      };
      var p = document.getElementById('p');
      p.addEventListener('click', function (e) {
        alert('父節點捕獲11')
      }, true);
      p.addEventListener('click', function (e) {
        alert('父節點冒泡')
      }, false);
    </script>
</body>
</html>

事實上,咱們忽略了一個重要的事實,發生在p元素的點擊時是先判斷是否處於事件階段二!!!

好比,咱們這裏點擊時候

這個時候,咱們已經處於了事件階段,因此根本不存在捕獲或者冒泡

這個時候會按照事件註冊順序,由事件隊列中取出回調函數,執行之!

這個是一個比較容易忽略的地方,安裝咱們臨時的理解,很容易忽略處於階段,而只關注捕獲階段以及冒泡階段,事實上處於階段時候的處理機制是不同的

兩個嵌套

爲了論證上面的說明,咱們分別下下下面的代碼:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title></title>
  <style type="text/css">
    #p { width: 300px; height: 300px; padding: 10px; border: 1px solid black; }
    #c { width: 200px; height: 200px; border: 1px solid red; }
    #sub { width: 100px; height: 100px; border: 1px solid red; }
  </style>
</head>
<body>
  <div id="p">
    parent
    <div id="c">
      child
    </div>
  </div>
  <script type="text/javascript">
    window.alert = function (msg) {
      console.log(msg);
    };
    var p = document.getElementById('p'),
        c = document.getElementById('c');
    p.addEventListener('click', function (e) {
      alert('父節點冒泡')
    }, false);

    c.addEventListener('click', function (e) {
      alert('子節點捕獲')
    }, true);
    c.addEventListener('click', function (e) {
      alert('子節點冒泡')
    }, false);
    p.addEventListener('click', function (e) {
      alert('父節點捕獲')
    }, true);
  </script>
</body>
</html>

這個時候,咱們點擊子div時候發生瞭如下事情

① 判斷e.target == e.currentTarget,發現不相等

② 根據相關判斷,發現處於捕獲階段(具體邏輯咱們後面來)

③ 觸發捕獲階段的各個事件

④ 進入處於階段,將此刻的事件隊列中的函數執行之

⑤ 進入冒泡階段......

好比稍做修改,上面的代碼輸出將會不同:

p.addEventListener('click', function (e) {
  alert('父節點冒泡')
}, false);

c.addEventListener('click', function (e) {
  alert('子節點冒泡')
}, false);
c.addEventListener('click', function (e) {
  alert('子節點捕獲')
}, true);

p.addEventListener('click', function (e) {
  alert('父節點捕獲')
}, true);

三層嵌套

根據以上理解,咱們很容易就能夠說出下述代碼的輸出

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <style type="text/css">
         #p { width: 300px; height: 300px; padding: 10px;  border: 1px solid black; }
         #c { width: 200px; height: 200px; border: 1px solid red; }
         #sub { width: 100px; height: 100px; border: 1px solid red; }
    </style>
</head>
<body>
    <div id="p">
        parent
        <div id="c">
            child
            <div id="sub">
            sub

        </div>
        </div>
    </div>
    <script type="text/javascript">

      window.alert = function (msg) {
        console.log(msg);
      };

      var p = document.getElementById('p'),
        c = document.getElementById('c'),
      sub = document.getElementById('sub');

      sub.addEventListener('click', function (e) {
        alert('sub節點冒泡')
      }, false);

      sub.addEventListener('click', function (e) {
        alert('sub節點捕獲')
      }, true);

      p.addEventListener('click', function (e) {
        alert('父節點捕獲11')
      }, true);

      p.addEventListener('click', function (e) {
        alert('父節點冒泡')
      }, false);

      c.addEventListener('click', function (e) {
        alert('子節點捕獲')
      }, true);

      c.addEventListener('click', function (e) {
        alert('子節點冒泡')
      }, false);

      p.addEventListener('click', function (e) {
        alert('父節點捕獲')
      }, true);

    </script>
</body>
</html>
View Code

模擬javascript事件機制

在此以前,咱們來講幾個基礎知識點

dom惟一標識

在頁面上的dom,每一個dom都應該有其惟一標識——_zid(咱們這裏統一爲_zid)/sourceIndex,可是多數瀏覽器可能認爲,這個接口並不須要告訴用戶因此咱們都不能得到

可是IE將這個接口放出來了——sourceIndex

咱們這裏以百度首頁爲例:

1 var doms = document.getElementsByTagName('*');
2 var str = '';
3 for (var i = 0, len = doms.length; i < len; i++) {
4     str += doms[i].tagName + ': ' + doms[i].sourceIndex + '\n';
5 }

能夠看到,越是上層的_zid越小

其實,dom _zid生成規則應該是以樹的正序而來(好像是吧.....),反正是從上到下,從左到右

有了這個後,咱們來看看咱們如何得到一個dom的註冊事件集合

獲取dom註冊事件集合

好比咱們爲一個dom同時綁定了2個click事件,又給他綁定一個keydown事件,那麼對於這個dom來講他就具備3個事件了

咱們有什麼辦法能夠得到一個dom註冊的事件呢???

答案很遺憾,瀏覽器都沒有放出api,因此咱們暫時不能知道一個dom到底被註冊了多少事件......

PS:若是您知道這個問題的答案,請留言

有了以上兩個知識點,咱們就能夠開始今天的扯淡了

注意:下文進入猜測時間

補充點

這裏經過園友 JexCheng 的提示,其實一些瀏覽器是提供了獲取dom事件節點的方法的

DOM API是沒有。不過瀏覽器提供了一個調試用的接口。
Chrome在console下能夠運行下面這個方法:
getEventListeners(node),
得到對象上綁定的全部事件監聽函數。

注意,是在console裏面執行getEventListeners方法
 1 <html xmlns="http://www.w3.org/1999/xhtml">
 2 <head>
 3   <title></title>
 4 </head>
 5 <body>
 6 <div id="d">ddssdsd</div>
 7   <script type="text/javascript">
 8     var node = document.getElementsByTagName('*');
 9     var d = document.getElementById('d');
10     d.addEventListener('click', function () {
11       alert();
12     }, false);
13     d.addEventListener('click', function () {
14       alert('我是第二次');
15     }, false);
16     d.onclick = function () {
17       alert('不規範的綁定');
18     }
19     d.addEventListener('click', function () {
20       alert();
21     }, true);
22 
23     d.addEventListener('mousedown', function () {
24       console.log('mousedown');
25     }, true);
26     var evets = typeof getEventListeners == 'function' && getEventListeners(d)
27   </script>
28 </body>
29 </html>

以上代碼在chrome中的console結果爲:

能夠看到,不管何種綁定,這裏都是能夠獲取的,並且獲取的對象與咱們模擬的對象比較接近

事件註冊發生的事

首先,咱們爲dom註冊事件的語法是:

1 dom.addEventListener('click', function () {
2     alert('ddd');
3 })

以上述代碼來講,我做爲瀏覽器,以這個代碼來講,在註冊階段我即可以保存如下信息:

 1 <html xmlns="http://www.w3.org/1999/xhtml">
 2 <head>
 3     <title></title>
 4     <style type="text/css">
 5          #p { width: 300px; height: 300px; padding: 10px;  border: 1px solid black; }
 6          #c { width: 100px; height: 100px; border: 1px solid red; }
 7     </style>
 8 </head>
 9 <body>
10     <div id="p">
11         parent
12         <div id="c">
13             child
14         </div>
15     </div>
16     <script type="text/javascript">
17         var p = document.getElementById('p'),
18         c = document.getElementById('c');
19         c.addEventListener('click', function () {
20             alert('子節點捕獲')
21         }, true);
22 
23         c.addEventListener('click', function () {
24             alert('子節點冒泡')
25         }, false);
26 
27         p.addEventListener('click', function () {
28             alert('父節點捕獲')
29         }, true);
30 
31         p.addEventListener('click', function () {
32             alert('父節點冒泡')
33         }, false);
34     </script>
35 </body>
36 </html>

這裏,咱們爲parent和child綁定了click事件,因此瀏覽器能夠得到以下隊列結構:

/****** 第一步-註冊事件 ******/
//頁面事件存儲在一個隊列裏
//以_zid排序
var eventQueue = [
  {
    _zid: 'parent',
    handlers: {
      click: [
        { useCapture: true, listener: fn },
        { useCapture: false, listener: fn },
        { useCapture: false, listener: fn }
      ]
    }
  },
  {
    _zid:'child',
    handlers:{
      click: [
      //......
      ]
    }
  },
  {
    _zid: '_zid',
    handlers: {
    //……
    }
  }
];

就那parent這個div來講,咱們爲他綁定了兩個click事件(咱們其實能夠綁定3個4個或者更多,因此事件集合是一個數組,執行具備前後順序)

其中註冊事件時候,又會分冒泡和捕獲,並且這裏以_zid排序(好比:document->body->div#p->div#c)

而後第一個階段就結束了

PS:我想底層c++語言必定有相似的這個隊列,並且能夠釋放接口,讓咱們獲取一個dom所註冊的全部事件

注意,此處隊列是這樣,可是咱們真正點擊一個元素,可能就只抽取其中一部分關聯的對象組成一個新的隊列,供下面使用

初始化事件參數

第二步就是初始化事件參數,咱們能夠經過addEventListener,建立事件參數,可是咱們這裏簡單模擬便可:

注意,爲了方便理解,咱們這裏暫不考慮mousedown

1 /****** 第二步-初始化事件參數 ******/
2 var Event = {};
3 Event.type = 'click';
4 Event.target = el;//當前手指點擊最深dom元素
5 //初始化信息
6 //......
7 //鼠標位置信息等

在這裏比較關鍵的就是咱們必定要好好定義咱們的target!!!

因而能夠進入咱們的關鍵步驟了,觸發事件

觸發事件

事件觸發分三步走,首先是捕獲而後是處於階段最後是冒泡階段:

 1 var isTarget = false;
 2 
 3 //當前處於的dom標識,根據他能夠獲取當前所處dom
 4 var cur_zid = null;
 5 
 6 //此時,咱們已經得到了e.target了,這個必定要注意
 7 Event.eventPhase = 1;
 8 
 9 //首先是捕獲階段,事件執行至event.target爲止,咱們這裏只關注click
10 //到event.target時,須要進行特殊處理
11 for (var index = 0, length = eventQueue.lenth; index < length; index++) {
12 
13   var obj = eventQueue[index];
14   cur_zid = obj._zid;
15 
16   //若是立刻便進入了處於階段,便無論捕獲階段下面的代碼了,不然便執行捕獲階段的事件
17   //執行至target便跳出循環,再也不執行下面的操做,/一旦處於階段便不執行相關事件之間跳出,並跳出捕獲階段,可是記錄當前索引值
18   if (Event.target._zid == eventQueue[index]._zid) {
19     Event.eventPhase = 2;//當前階段
20     isTarget = true;
21     break;
22   }
23 
24   //如果當前已是target便再也不向下捕獲
25   if(isTarget) break;
26 
27   //獲取捕獲時期該元素的click事件集合,這裏須要作必定篩選
28   var clickHandlers = [];
29   for(var k in obj.handlers.click) {
30     if(obj.handlers.click[k].useCapture == false) clickHandlers.push(obj.handlers.click[k].listener);
31   }
32 
33   //事件處理程序根據當前zid獲取正在處理的那個元素,固然這個方法,此處並未實現
34   Event.currentTarget = getRealDom(cur_zid); 
35   for (var i = 0, len = clickHandlers.length; i < len; i++) {
36     //每一次事件執行均可能更改Event的值
37     clickHandlers[i](Event);
38   }
39 
40   //若是阻止冒泡,跳出全部循環,不執行後面的事件
41   if (Event.bubbles) {
42     //不在執行以後邏輯直接跳出
43     //return;
44   }
45 }
46 
47 //處於事件階段的相關事件按註冊順序取出,由於前面保留了index,這裏直接使用便可
48 //而這裏的obj保存的就是處於階段時候的相關對象,由於前面跳出了......
49 var cur_clickHandlers = obj.handlers.click;
50 
51 //事件處理程序根據當前zid獲取正在處理的那個元素,固然這個方法,此處並未實現
52 Event.currentTarget = getRealDom(cur_zid); 
53 for (var i = 0, len = cur_clickHandlers.length; i < len; i++) {
54   cur_clickHandlers[i](Event);
55 }
56 
57 //若是阻止冒泡,跳出全部循環,不執行後面的事件
58 if (Event.bubbles) {
59   //不在執行以後邏輯直接跳出
60   //return;
61 }
62 
63 Event.eventPhase = 3;
64 
65 //冒泡階段
66 for(var index = eventQueue.lenth; index != 0; index--) {
67 
68   var obj = eventQueue[index];
69   cur_zid = obj._zid;
70 
71   //若是再次到底處於階段直接跳出
72   if (Event.target._zid == eventQueue[index]._zid) {
73    return ;
74   }
75 
76   //獲取冒泡時期該元素的click事件集合,這裏須要作必定篩選
77   var clickHandlers = [];
78   for (var k in obj.handlers.click) {
79     if (obj.handlers.click[k].useCapture == true) clickHandlers.push(obj.handlers.click[k].listener);
80   }
81 
82   //事件處理程序根據當前zid獲取正在處理的那個元素,固然這個方法,此處並未實現
83   Event.currentTarget = getRealDom(cur_zid);
84   for (var i = 0, len = clickHandlers.length; i < len; i++) {
85     //每一次事件執行均可能更改Event的值
86     clickHandlers[i](Event);
87   }
88 
89   //若是阻止冒泡,跳出全部循環,不執行後面的事件
90   if (Event.bubbles) {
91     //不在執行以後邏輯直接跳出
92     //return;
93   }
94 }

這個註釋寫的很清楚了應該能表達清楚個人意思,對上次的想法作了調整

若是您對此有何想法,或者我這麼想是錯誤的,請指出!!!

結語

根據今天狗蛋問的問題,已經結合其它博主的想法,寫了這篇博客,但願對您有用,如何文章有誤,請不吝指出! 

相關文章
相關標籤/搜索