前言
又到了扯淡時間了,我最近在思考javascript事件機制底層的實現,但是暫時沒有勇氣去看chrome源碼,所以今天我來猜測一把
我們今天來猜一猜,探討探討,javascript底層事件機制是如何實現的
博客里面關於事件綁定與執行順序一塊理解有誤,請看最新博客
基礎知識
事件捕獲/冒泡
我們點擊一個span,我可能就想點擊一個span,事實上他是先點擊document,然后點擊事件傳遞到span的,而且並不會在span停下,span有子元素就會繼續往下,最后會依次回傳至document,我們這里偷一張圖:
我們這里偷了一張圖,這張圖很好的說明了事件的傳播方式
事件冒泡即由最具體的元素(文檔嵌套最深節點)接收,然后逐步上傳至document
事件捕獲會由最先接收到事件的元素然后傳向最里邊(我們可以將元素想象成一個盒子裝一個盒子,而不是一個積木堆積)
這里我們進入dom事件流,這里我們詳細看看javascript事件的傳遞方式
DOM事件流
DOM2級事件規定事件包括三個階段:
① 事件捕獲階段
② 處於目標階段
③ 事件冒泡階段
事件對象
所謂事件對象,是與特定對象相關,並且包含該事件詳細信息的對象。
事件對象作為參數傳遞給事件處理程序(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源碼了,這個留待我們后面解答。
如果您對此文中的想法有和意見或者建議,請留言