【探討】javascript事件機制底層實現原理

前言

又到了扯淡時間了,我最近在思考javascript事件機制底層的實現,可是暫時沒有勇氣去看chrome源碼,因此今天我來猜想一把javascript

咱們今天來猜一猜,探討探討,javascript底層事件機制是如何實現的css

博客裏面關於事件綁定與執行順序一塊理解有誤,請看最新博客html

基礎知識

事件捕獲/冒泡

咱們點擊一個span,我可能就想點擊一個span,事實上他是先點擊document,而後點擊事件傳遞到span的,並且並不會在span停下,span有子元素就會繼續往下,最後會依次回傳至document,咱們這裏偷一張圖:java

咱們這裏偷了一張圖,這張圖很好的說明了事件的傳播方式node

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

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

這裏咱們進入dom事件流,這裏咱們詳細看看javascript事件的傳遞方式c++

DOM事件流

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

① 事件捕獲階段chrome

② 處於目標階段api

③ 事件冒泡階段數組

事件對象

所謂事件對象,是與特定對象相關,而且包含該事件詳細信息的對象。

事件對象做爲參數傳遞給事件處理程序(IE8以前經過window.event得到),全部事件對象都有事件類型type與事件目標target(IE8以前的srcElement咱們不關注了)

各個事件的事件參數不同,好比鼠標事件就會有相關座標,包含和建立他的特定事件有關的屬性和方法,觸發的事件不同,參數也不同(好比鼠標事件就會有座標信息),咱們這裏題幾個較重要的

PS:如下的兄弟所有是隻讀的,因此不要妄想去隨意更改,IE以前的問題咱們就不關注了

bubbles

代表事件是否冒泡

cancelable

代表是否能夠取消事件的默認行爲

currentTarget

某事件處理程序當前正在處理的那個元素

defaultPrevented

爲true代表已經調用了preventDefault(DOM3新增)

eventPhase

調用事件處理程序的階段:1 捕獲;2 處於階段;3 冒泡階段

這個屬性的變化須要在斷點中查看,否則你看到的老是0

target

事件目標(綁定事件那個dom)

trusted

true代表是系統的,false爲開發人員自定義的(DOM3新增)

type

事件類型

view

與事件關聯的抽象視圖,發生事件的window對象

preventDefault

取消事件默認行爲,cancelable是true時可使用

stopPropagation

取消事件捕獲/冒泡,bubbles爲true才能使用

stopImmediatePropagation

取消事件進一步冒泡,而且組織任何事件處理程序被調用(DOM3新增)

在咱們的事件處理內部,this與currentTarget相同

模擬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事件,因此瀏覽器能夠得到以下隊列結構:

 1 /****** 第一步-註冊事件 ******/
 2 //頁面事件存儲在一個隊列裏
 3 //以_zid排序
 4 var eventQueue = [
 5   {
 6     _zid: 'parent',
 7     handlers: {
 8       click: {
 9         captrue: [fn, fn],
10         bubble: [fn, fn]
11       }
12     }
13   },
14   {
15     _zid:'child',
16     handlers:{
17       click: {
18         captrue: [],
19         bubble: []
20       }
21     }
22   },
23   {
24     _zid: '_zid',
25     handlers: {
26     //……
27     }
28   }
29 ];

就那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 /****** 第三步-觸發事件 ******/
 2 var isTarget = false;
 3 Event.eventPhase = 1;
 4 //首先是捕獲階段,事件執行至event.target爲止,咱們這裏只關注click
 5 for (var index = 0, length = eventQueue.lenth; index < length; index++) {
 6   //獲取捕獲時期該元素的click事件集合
 7   var clickHandlers = eventQueue[index].handlers.click.captrue;
 8   for (var i = 0, len = clickHandlers.length; i < len; i++) {
 9     Event.currentTarget = clickHandlers[i]; //事件處理程序當前正在處理的那個元素
10     //執行至target便跳出循環,再也不執行下面的操做
11     if (Event.target._zid == eventQueue[index]._zid) {
12       Event.eventPhase = 2;//當前階段
13       isTarget = true;
14     }
15     //執行綁定事件
16     clickHandlers[i](Event);
17     //若是阻止冒泡,跳出全部循環,不執行後面的事件
18     if (Event.bubbles) {
19       return;
20     }
21   }
22   //如果當前已是target便再也不向下捕獲
23   if(isTarget) break;
24 }
25 Event.eventPhase = 3;
26 //冒泡階段
27 for(var index = eventQueue.lenth; index !=0; index--) {
28   //若是zid小於等於當前元素,說明不須要處理
29   if(eventQueue[index]._zid <= Event.target._zid) continue;
30   //須要處理的部分了
31   var clickHandlers = eventQueue[index].handlers.click.bubble;
32  
33   //此段代碼能夠重構,暫時無論
34   for (var i = 0, len = clickHandlers.length; i < len; i++) {
35     Event.currentTarget = clickHandlers[i]; //事件處理程序當前正在處理的那個元素
36     //執行綁定事件
37     clickHandlers[i](Event);
38     //若是阻止冒泡,跳出全部循環,不執行後面的事件
39     if (Event.bubbles) {
40       return;
41     }
42   }
43 }

這個註釋寫的很清楚了應該能表達清楚個人意思,因而咱們這裏就簡單的模擬了事件機制的底層原理了:)

PS:若是您以爲不對,請留言

驗證猜測

如今,基礎理論提出來了,咱們須要驗證下這個想法是否站得住腳,因此這裏提了幾個例子,首先咱們回到上面的問題吧

驗證一:點擊問題

http://sandbox.runjs.cn/show/pesvelp1

首先咱們來看這個問題,咱們分別爲parent與child註冊了兩個click事件,一次冒泡一次捕獲

當咱們點擊父元素時,咱們按照理論的執行邏輯以下:

開始遍歷事件隊列(由document開始)

當遍歷對象若是註冊了click事件就會觸發,若是阻止了冒泡,執行後便跳出循環再也不執行

由於以前並無註冊事件,因此直接到了parent,這裏發現parent的_zid與target的_zid相等

因而便將狀態置爲處於目標階段,並打上標記跳出捕獲循環,再也不執行後面的事件句柄

Event.eventPhase = 2;//當前階段
isTarget = true;

捕獲結束後,開始執行冒泡的事件,循環由後向前,開始是child的click事件,可是此時child的_zid大於target的_zid因此繼續循環

最後會執行parent以上的dom註冊的click事件,沒有就算了

至於點擊child的邏輯咱們這裏就不分析了

驗證二:忽然移除dom

咱們這裏對上題作一個變形,咱們在parent點擊時候(捕獲階段)將child div給刪除,看看有什麼狀況

http://sandbox.runjs.cn/show/f1ke5vp8

 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             p.removeChild(c);
30         }, true);
31 
32         p.addEventListener('click', function () {
33             alert('父節點冒泡')
34         }, false);
35     </script>
36 </body>
37 </html>

其實這裏還有一個優化點,相信你們都知道:

移除dom並不會移除事件句柄,這個必須手動釋放

就是由於這個緣由,咱們的整個邏輯仍然會執行,各位本身能夠試試

驗證三:child阻止冒泡

咱們這裏再將上題稍加變形,在child 冒泡階段組織冒泡,其實這個不用說,parent的click不會執行

 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 (e) {
24             alert('子節點冒泡')
25             e.stopPropagation();
26         }, false);
27 
28         p.addEventListener('click', function () {
29             alert('父節點捕獲')
30         }, true);
31 
32         p.addEventListener('click', function () {
33             alert('父節點冒泡')
34         }, false);
35     </script>
36 </body>
37 </html>

驗證四:模擬click事件

 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         alert = function (msg) {
18             console.log(msg);
19         }
20 
21         var p = document.getElementById('p'),
22         c = document.getElementById('c');
23         c.addEventListener('click', function (e) {
24             console.log(e);
25             alert('子節點捕獲')
26         }, true);
27         c.addEventListener('click', function (e) {
28             console.log(e);
29             alert('子節點冒泡')
30         }, false);
31 
32         p.addEventListener('click', function (e) {
33             console.log(e);
34             alert('父節點捕獲')
35         }, true);
36 
37         p.addEventListener('click', function (e) {
38             console.log(e);
39             alert('父節點冒泡')
40         }, false);
41 
42         document.addEventListener('keydown', function (e) {
43             if (e.keyCode == '32') {
44                 var type = 'click'; //要觸發的事件類型
45                 var bubbles = true; //事件是否能夠冒泡
46                 var cancelable = true; //事件是否能夠阻止瀏覽器默認事件
47                 var view = document.defaultView; //與事件關聯的視圖,該屬性默認便可,無論
48                 var detail = 0;
49                 var screenX = 0;
50                 var screenY = 0;
51                 var clientX = 0;
52                 var clientY = 0;
53                 var ctrlKey = false; //是否按下ctrl
54                 var altKey = false; //是否按下alt
55                 var shiftKey = false;
56                 var metaKey = false;
57                 var button = 0; //表示按下哪個鼠標鍵
58                 var relatedTarget = 0; //模擬mousemove或者out時候用到,與事件相關的對象
59                 var event = document.createEvent('Events');
60                 event.myFlag = '葉小釵';
61                 event.initEvent(type, bubbles, cancelable, view, detail, screenX, screenY, clientX, clientY,
62 ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget);
63                 
64                 console.log(event);
65                 c.dispatchEvent(event);
66             }
67         }, false);
68     </script>
69 </body>
70 </html>

http://sandbox.runjs.cn/show/pesvelp1

咱們最後模擬一下click事件,這裏按空格便會觸發child的click事件,這裏依然走咱們上述邏輯

因此,咱們今天到此爲止

結語

今天,咱們一塊兒模擬猜想了javascript事件機制的底層實現,這裏只作了最簡單最單純的模擬

好比兩個平級dom(div)點擊時候這裏的算法就有一點問題,可是無傷大雅,探討嘛,至於事情的真相如何,這裏就只能拋磚引玉了。

正確答案要須要看chrome源碼了,這個留待咱們後面解答。

若是您對此文中的想法有和意見或者建議,請留言

相關文章
相關標籤/搜索