【探討】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

我們這里以百度首頁為例:

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為止,我們這里只關注click5 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源碼了,這個留待我們后面解答。

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

轉載于:https://www.cnblogs.com/liangshuang/p/8473324.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/250931.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/250931.shtml
英文地址,請注明出處:http://en.pswp.cn/news/250931.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

node --- 在node中使用mongoosemongoDB的安裝

*首先確保,你的電腦安裝了mongodb,網址: mongodb官網 *使用npm安裝 mongoose: mongoose官網 ps:mongoose是Node中操作mongoDB的第三方插件.用于提高數據庫操作效率(相當于在mongoDB上封裝了一次,暴露出更友好的API) MongoDB的安裝 1.下載地址 2.下載好了后,傻瓜式的安裝(我的…

websocket demo

git node.js創建websocket 的服務 Nodejs Websocket包 ws.createServer([options], [callback]) The callback is a function which is automatically added to the “connection” event. 前端代碼 1. 創建實例、打開連接 this.websocket new WebSocket(ws://127.0.0.1:80…

shell常用命令總結總結

打rpm包&#xff1a; rpmbuild -bb SPECS/smplayer.spec --define "_topdir pwd" 安裝rpm包&#xff1a; rpm -ivh [rpm包文件] 如果安裝不上 rpm -ivh [rpm包文件] --force #強制安裝 打包的時候可能需要一些依賴&#xff1a; dnf install 【依賴文件名】 sed -i常用…

Filter

一、簡介 Filter也稱之為過濾器&#xff0c;它是Servlet技術中最激動人心的技術&#xff0c;WEB開發人員通過Filter技術&#xff0c;對web服務器管理的所有web資源&#xff1a;例如Jsp&#xff0c;Servlet&#xff0c;靜態圖片文件或靜態html文件進行攔截&#xff0c;從而實現一…

前端面試手寫題

深拷貝 // 深拷貝 function deepClone(ori) {let tar;if (typeof ori object && ori ! null) {tar Array.isArray(ori) ? [] : {}for (let k in ori) {if (ori.hasOwnProperty(k)) {tar[k] deepClone(ori[k])}}} else {tar ori}return tar}繼承 // 圣杯模式實現…

node --- 使用express.Router與body-parser

express框架提供了一個Router方法,用于監聽路由 // 命令行(windows*64) npm install express --save// router.js const express require("express"); // 定義路由 const router express.Router();// 處理http://host:port/students/ 路由(GET方法) router.get…

python基礎1 第一天

TEST 1 阿斯蒂芬 day1test1 while 1&#xff1a;print&#xff08;333&#xff09; import randomprint轉載于:https://www.cnblogs.com/shuangzhu/p/9243853.html

【數據庫】《SQL必知必會 4th》部分筆記

9.匯總數據 count(*) 包括空 count(name) 不包括空 10.分組數據 group by 分組 having 過濾分組 where 過濾行 11.子查詢 select .. from .. where in (select ...) 由內向外處理 A.子查詢過濾 作為子查詢的語句只能查詢單個列。 B.作為計算字段使用子查詢 select cust_name, …

微軟認知服務應用秘籍 – 漫畫翻譯篇

概述 微軟認知服務包括了影像、語音、語言、搜索、知識五大領域&#xff0c;通過對這些認知服務的獨立或者組合使用&#xff0c;可以解決很多現實世界中的問題。作為AI小白&#xff0c;我們可以選擇艱難地攀登崇山峻嶺&#xff0c;也可以選擇像牛頓一樣站在巨人的肩膀上。本章節…

01 React初步認知、React元素、渲染、工程化

定義 react&#xff1a;用于構建用戶界面的 JavaScript 庫 &#xff08;僅負責View層渲染、應在視圖上體現交互邏輯&#xff09;vue&#xff1a;漸進式JavaScript 框架&#xff08;MVVM&#xff09; 使用 引入CDN腳本添加根容器 div #app創建React組件 ReactDOM.render Re…

node --- 在express中配置使用模板引擎(art-template)

下載依賴: npm install --save art-template express-art-template配置: // app.js const express require("express"); const app express(); app.engine("html", require("express-art-template"));使用: 例如處理瀏覽器GET請求 /students…

PAM認證機制

一、PAM簡介 Sun公司1995年開發的一種與認證相關的通用框架機制&#xff0c;PAM只關注如何為服務驗證用戶的API&#xff0c;通過提供一些動態鏈接庫和一套統一的API&#xff0c;將系統提供的服務和該服務的認證方式分開&#xff1b;PAM只是一個框架而已&#xff0c;自身不做認證…

02 JSX學習

使用vite處理jsx vite引入的腳本必須是ESM的 npm init -y yarn add vite package.json 添加vite命令 index.html引入jsxJSX是什么 一種標簽語法&#xff0c;在JS基礎上進行的語法擴展不是字符串、也不是HTML是描述UI呈現與交互的直觀的表現形式JSX被編譯后會生成React元素 &am…

使用FreeCookies 控制瀏覽器cookies及修改http響應內容

FreeCookies 插件安裝 1&#xff1a;您的計算機需要已經安裝Fiddler &#xff08;如未安裝&#xff0c;請至官網下載安裝 http://docs.telerik.com/fiddler/configure-fiddler/tasks/configurefiddler&#xff09; 2&#xff1a;進入Fiddler安裝目錄下的Scripts目錄下&#xff…

node --- 使用node連接mysql

1.確保下載了mysql,且mysql處于打開狀態. 2.確保下載了node,并成功安裝:https://nodejs.org/en/ (小黑窗 node -v 查看) 3.安裝node操作mysql的依賴包: # 命令行 npm install --save -mysql# 注:如果沒有package.json 建議先使用 npm init -y 初始化正題 // app.js// 1. 引…

03 渲染元素ReactDOM.render

React與ReactDOM是2個不同的庫&#xff0c;根節點內的所有內容&#xff08;和DOM更新、渲染相關&#xff09;由ReactDOM來管理一個React應用只有一個根節點用ReactDOM.render將React元素渲染到根節點 ReactDOM.render 參數1 React元素&#xff08;React.createElement(類組件/…

javascript --- 異步按順序執行

使用promise可以很優雅的封裝一個異步函數,使其按指定順序執行: // 異步讀取文件操作 const fs require("fs"); function promiseReadFile(url) {return new Promise(function (resolve, reject) {fs.readFile(url, function(err, data) {if(err) {reject(err);} e…

web提高:負載均衡

1、集群 1、為什么建議在阿里云購買負載均衡 非常便宜&#xff0c;又好用&#xff0c;有穩定&#xff0c;有簡單。自己搭建不了負載均衡&#xff0c;因為共有云不支持組播跑不了vrp協議。你不會集群的概念&#xff0c;你還是蒙蒙的。2、為什么使用集群&#xff1f; 1、小規模 …

node --- 一個很好用的包json-server

這個第三方包,可以將json文件暴露出來,用http獲取. (data.json如下) 下載依賴: npm install --g json-server查看是否含有json-server json -sever --version啟動json-server 參考:https://www.npmjs.com/package/json-server

04 組件與Props

一些概念 組件&#xff1a;視圖的片段、內部管理數據集合&#xff08;state&#xff09;外部傳入配置結合&#xff08;props&#xff09;包含&#xff1a; 1. 視圖標記&#xff08;React的JSX、Vue的template&#xff09;需要經過轉換而成為真實的DOM 事件 數據 邏輯&#x…