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


前言

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

結果被團隊的狗蛋讀了,發現其中一塊”特別“的地方,然后之后讀了Barret Lee 的一篇博客:[解惑]JavaScript事件機制

發現該博主之前對這個問題可能也有一定”誤解“

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

所以,這里就拿出來說說,各位看看就好

事件階段

引用:

群里童鞋問到關於事件傳播的一個問題:“事件捕獲的時候,阻止冒泡,事件到達目標之后,還會冒泡嗎?”。

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

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

本文地址: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 }

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

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

結語

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM