javascript事件機制底層實現原理
前言
又到了扯淡時間了,我最近在思考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
我們這裡以百度首頁為例:
var doms = document.getElementsByTagName('*');
var str = '';
for (var i = 0, len = doms.length; i < len; i++) {
str += doms[i].tagName + ': ' + doms[i].sourceIndex + '\n';
}
可以看到,越是上層的_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方法
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
</head>
<body>
<div id="d">ddssdsd</div>
<script type="text/javascript">
var node = document.getElementsByTagName('*');
var d = document.getElementById('d');
d.addEventListener('click', function () {
alert();
}, false);
d.addEventListener('click', function () {
alert('我是第二次');
}, false);
d.onclick = function () {
alert('不規範的繫結');
}
d.addEventListener('click', function () {
alert();
}, true);
d.addEventListener('mousedown', function () {
console.log('mousedown');
}, true);
var evets = typeof getEventListeners == 'function' && getEventListeners(d)
</script>
</body>
</html>
以上程式碼在chrome
中的console
結果為:
可以看到,無論何種繫結,這裡都是可以獲取的,而且獲取的物件與我們模擬的物件比較接近
事件註冊發生的事
首先,我們為dom註冊事件的語法是:
dom.addEventListener('click', function () {
alert('ddd');
})
以上述程式碼來說,我作為瀏覽器,以這個程式碼來說,在註冊階段我便可以儲存以下資訊:
<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: 100px; height: 100px; border: 1px solid red; }
</style>
</head>
<body>
<div id="p">
parent
<div id="c">
child
</div>
</div>
<script type="text/javascript">
var p = document.getElementById('p'),
c = document.getElementById('c');
c.addEventListener('click', function () {
alert('子節點捕獲')
}, true);
c.addEventListener('click', function () {
alert('子節點冒泡')
}, false);
p.addEventListener('click', function () {
alert('父節點捕獲')
}, true);
p.addEventListener('click', function () {
alert('父節點冒泡')
}, false);
</script>
</body>
</html>
這裡,我們為parent
和child
繫結了click
事件,所以瀏覽器可以獲得如下佇列結構:
/****** 第一步-註冊事件 ******/
//頁面事件儲存在一個佇列裡
//以_zid排序
var eventQueue = [
{
_zid: 'parent',
handlers: {
click: {
captrue: [fn, fn],
bubble: [fn, fn]
}
}
},
{
_zid:'child',
handlers:{
click: {
captrue: [],
bubble: []
}
}
},
{
_zid: '_zid',
handlers: {
//……
}
}
];
就那parent
這個div
來說,我們為他繫結了兩個click
事件(我們其實可以繫結3個4個或者更多,所以事件集合是一個陣列,執行具有先後順序)
其中註冊事件時候,又會分冒泡和捕獲,而且這裡以_zid排序(比如:document->body->div#p->div#c)
然後第一個階段就結束了
PS:我想底層c++語言一定有類似的這個佇列,而且可以釋放介面,讓我們獲取一個dom所註冊的所有事件
注意,此處佇列是這樣,但是我們真正點選一個元素,可能就只抽取其中一部分關聯的物件組成一個新的佇列,供下面使用
初始化事件引數
第二步就是初始化事件引數,我們可以通過addEventListener
,建立事件引數,但是我們這裡簡單模擬即可:
注意,為了方便理解,我們這裡暫不考慮mousedown
/****** 第二步-初始化事件引數 ******/
var Event = {};
Event.type = 'click';
Event.target = el;//當前手指點選最深dom元素
//初始化資訊
//......
//滑鼠位置資訊等
在這裡比較關鍵的就是我們一定要好好定義我們的target
!!!
於是可以進入我們的關鍵步驟了,觸發事件
觸發事件
事件觸發分三步走,首先是捕獲然後是處於階段最後是冒泡階段:
/****** 第三步-觸發事件 ******/
var isTarget = false;
Event.eventPhase = 1;
//首先是捕獲階段,事件執行至event.target為止,我們這裡只關注click
for (var index = 0, length = eventQueue.lenth; index < length; index++) {
//獲取捕獲時期該元素的click事件集合
var clickHandlers = eventQueue[index].handlers.click.captrue;
for (var i = 0, len = clickHandlers.length; i < len; i++) {
Event.currentTarget = clickHandlers[i]; //事件處理程式當前正在處理的那個元素
//執行至target便跳出迴圈,不再執行下面的操作
if (Event.target._zid == eventQueue[index]._zid) {
Event.eventPhase = 2;//當前階段
isTarget = true;
}
//執行繫結事件
clickHandlers[i](Event);
//如果阻止冒泡,跳出所有迴圈,不執行後面的事件
if (Event.bubbles) {
return;
}
}
//若是當前已經是target便不再向下捕獲
if(isTarget) break;
}
Event.eventPhase = 3;
//冒泡階段
for(var index = eventQueue.lenth; index !=0; index--) {
//如果zid小於等於當前元素,說明不需要處理
if(eventQueue[index]._zid <= Event.target._zid) continue;
//需要處理的部分了
var clickHandlers = eventQueue[index].handlers.click.bubble;
//此段程式碼可以重構,暫時不管
for (var i = 0, len = clickHandlers.length; i < len; i++) {
Event.currentTarget = clickHandlers[i]; //事件處理程式當前正在處理的那個元素
//執行繫結事件
clickHandlers[i](Event);
//如果阻止冒泡,跳出所有迴圈,不執行後面的事件
if (Event.bubbles) {
return;
}
}
}
這個註釋寫的很清楚了應該能表達清楚我的意思,於是我們這裡就簡單的模擬了事件機制的底層原理了:)
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
<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: 100px; height: 100px; border: 1px solid red; }
</style>
</head>
<body>
<div id="p">
parent
<div id="c">
child
</div>
</div>
<script type="text/javascript">
var p = document.getElementById('p'),
c = document.getElementById('c');
c.addEventListener('click', function () {
alert('子節點捕獲')
}, true);
c.addEventListener('click', function () {
alert('子節點冒泡')
}, false);
p.addEventListener('click', function () {
alert('父節點捕獲')
p.removeChild(c);
}, true);
p.addEventListener('click', function () {
alert('父節點冒泡')
}, false);
</script>
</body>
</html>
其實這裡還有一個優化點,相信大家都知道:
移除dom並不會移除事件控制程式碼,這個必須手動釋放
就是因為這個原因,我們的整個邏輯仍然會執行,各位自己可以試試
驗證三:child
阻止冒泡
我們這裡再將上題稍加變形,在child
冒泡階段組織冒泡,其實這個不用說,parent
的click
不會執行
<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: 100px; height: 100px; border: 1px solid red; }
</style>
</head>
<body>
<div id="p">
parent
<div id="c">
child
</div>
</div>
<script type="text/javascript">
var p = document.getElementById('p'),
c = document.getElementById('c');
c.addEventListener('click', function () {
alert('子節點捕獲')
}, true);
c.addEventListener('click', function (e) {
alert('子節點冒泡')
e.stopPropagation();
}, false);
p.addEventListener('click', function () {
alert('父節點捕獲')
}, true);
p.addEventListener('click', function () {
alert('父節點冒泡')
}, false);
</script>
</body>
</html>
驗證四:模擬click事件
<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: 100px; height: 100px; border: 1px solid red; }
</style>
</head>
<body>
<div id="p">
parent
<div id="c">
child
</div>
</div>
<script type="text/javascript">
alert = function (msg) {
console.log(msg);
}
var p = document.getElementById('p'),
c = document.getElementById('c');
c.addEventListener('click', function (e) {
console.log(e);
alert('子節點捕獲')
}, true);
c.addEventListener('click', function (e) {
console.log(e);
alert('子節點冒泡')
}, false);
p.addEventListener('click', function (e) {
console.log(e);
alert('父節點捕獲')
}, true);
p.addEventListener('click', function (e) {
console.log(e);
alert('父節點冒泡')
}, false);
document.addEventListener('keydown', function (e) {
if (e.keyCode == '32') {
var type = 'click'; //要觸發的事件型別
var bubbles = true; //事件是否可以冒泡
var cancelable = true; //事件是否可以阻止瀏覽器預設事件
var view = document.defaultView; //與事件關聯的檢視,該屬性預設即可,不管
var detail = 0;
var screenX = 0;
var screenY = 0;
var clientX = 0;
var clientY = 0;
var ctrlKey = false; //是否按下ctrl
var altKey = false; //是否按下alt
var shiftKey = false;
var metaKey = false;
var button = 0; //表示按下哪一個滑鼠鍵
var relatedTarget = 0; //模擬mousemove或者out時候用到,與事件相關的物件
var event = document.createEvent('Events');
event.myFlag = '葉小釵';
event.initEvent(type, bubbles, cancelable, view, detail, screenX, screenY, clientX, clientY,
ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget);
console.log(event);
c.dispatchEvent(event);
}
}, false);
</script>
</body>
</html>
http://sandbox.runjs.cn/show/pesvelp1
我們最後模擬一下click
事件,這裡按空格便會觸發child
的click
事件,這裡依然走我們上述邏輯
所以,我們今天到此為止
結語
今天,我們一起模擬猜測了javascript
事件機制的底層實現,這裡只做了最簡單最單純的模擬
比如兩個平級dom(div)點選時候這裡的演算法就有一點問題,但是無傷大雅,探討嘛,至於事情的真相如何,這裡就只能拋磚引玉了。
相關文章
- 深入詳解Java反射機制與底層實現原理?Java反射
- Java 併發機制底層實現 —— volatile 原理、synchronize 鎖優化機制Java優化
- 併發機制的底層實現
- 【讀書筆記】Java併發機制的底層實現原理筆記Java
- HashMap底層實現原理HashMap
- NSDictionary底層實現原理
- AutoreleasePool底層實現原理
- php底層原理之垃圾回收機制PHP
- iOS底層原理總結篇-- 深入理解 KVC\KVO 實現機制iOS
- Redisson的看門狗機制底層實現Redis
- MySQL索引底層實現原理MySql索引
- ArrayList底層的實現原理
- PHP底層的執行機制與原理PHP
- PHP 底層的執行機制與原理PHP
- JavaScript 閉包的底層執行機制JavaScript
- JavaScript閉包的底層執行機制JavaScript
- 初步理解 JavaScript 底層原理JavaScript
- Java 底層機制Java
- HashMap的底層結構、原理、擴容機制HashMap
- MySQL Join的底層實現原理MySql
- day13-實現Spring底層機制-03Spring
- PHP 底層的執行機制與原理解析PHP
- php底層原理之陣列實現PHP陣列
- KVO的使用和底層實現原理
- Mysql鎖機制與最佳化實踐以及MVCC底層原理剖析MySqlMVC
- 【併發程式設計】(二)Java併發機制底層實現原理——synchronized關鍵字程式設計Javasynchronized
- day08-SpringMVC底層機制簡單實現-04SpringMVC
- iOS底層-KVC使用實踐以及實現原理iOS
- iOS底層原理總結 - 關聯物件實現原理iOS物件
- React-Router底層原理分析與實現React
- 面試必問:HashMap 底層實現原理分析面試HashMap
- MG--探究KVO的底層實現原理
- 圖解Android事件分發機制(深入底層原始碼)圖解Android事件原始碼
- Netty原始碼解析 -- 事件迴圈機制實現原理Netty原始碼事件
- iOS開發·KVO用法,原理與底層實現: runtime模擬實現KVO監聽機制(Blcok及Delgate方式)iOS
- iOS窺探KVO底層實現原理篇iOS
- 面試官:說說反射的底層實現原理?面試反射
- [Java併發系列] 1.Java併發機制的底層實現Java