又到了扯淡时间了,我最近在思考javascript事件机制底层的实现,可是暂时没有勇气去看chrome源码,因此今天我来猜想一把javascript
咱们今天来猜一猜,探讨探讨,javascript底层事件机制是如何实现的css
博客里面关于事件绑定与执行顺序一块理解有误,请看最新博客html
咱们点击一个span,我可能就想点击一个span,事实上他是先点击document,而后点击事件传递到span的,并且并不会在span停下,span有子元素就会继续往下,最后会依次回传至document,咱们这里偷一张图:java
咱们这里偷了一张图,这张图很好的说明了事件的传播方式node
事件冒泡即由最具体的元素(文档嵌套最深节点)接收,而后逐步上传至document
事件捕获会由最早接收到事件的元素而后传向最里边(咱们能够将元素想象成一个盒子装一个盒子,而不是一个积木堆积)
这里咱们进入dom事件流,这里咱们详细看看javascript事件的传递方式c++
DOM2级事件规定事件包括三个阶段:算法
① 事件捕获阶段chrome
② 处于目标阶段api
③ 事件冒泡阶段数组
所谓事件对象,是与特定对象相关,而且包含该事件详细信息的对象。
事件对象做为参数传递给事件处理程序(IE8以前经过window.event得到),全部事件对象都有事件类型type与事件目标target(IE8以前的srcElement咱们不关注了)
各个事件的事件参数不同,好比鼠标事件就会有相关坐标,包含和建立他的特定事件有关的属性和方法,触发的事件不同,参数也不同(好比鼠标事件就会有坐标信息),咱们这里题几个较重要的
PS:如下的兄弟所有是只读的,因此不要妄想去随意更改,IE以前的问题咱们就不关注了
代表事件是否冒泡
代表是否能够取消事件的默认行为
某事件处理程序当前正在处理的那个元素
为true代表已经调用了preventDefault(DOM3新增)
调用事件处理程序的阶段:1 捕获;2 处于阶段;3 冒泡阶段
这个属性的变化须要在断点中查看,否则你看到的老是0
事件目标(绑定事件那个dom)
true代表是系统的,false为开发人员自定义的(DOM3新增)
事件类型
与事件关联的抽象视图,发生事件的window对象
取消事件默认行为,cancelable是true时可使用
取消事件捕获/冒泡,bubbles为true才能使用
取消事件进一步冒泡,而且组织任何事件处理程序被调用(DOM3新增)
在咱们的事件处理内部,this与currentTarget相同
在此以前,咱们来讲几个基础知识点
在页面上的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同时绑定了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的逻辑咱们这里就不分析了
咱们这里对上题作一个变形,咱们在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 冒泡阶段组织冒泡,其实这个不用说,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>
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源码了,这个留待咱们后面解答。
若是您对此文中的想法有和意见或者建议,请留言