談起JavaScript的事件,事件冒泡、事件捕獲、事件委託這三個話題,無論是面試還是在平時的JavaScript的開發中,都很難避免,不太熟悉JavaScript中的這些事件的同學,我們一起來看看本篇文章吧!
事件流
JavaScript與HTML之間的互動是透過事件實現的。事件,就是文件或瀏覽器視窗中發生的一些特定的互動瞬間。可以使用偵聽器預訂事件,以便在事件發生時執行對應的程式碼。
事件流的起源:就是在瀏覽器發展到第四代的時候,瀏覽器開發團隊遇到一個問題:頁面的哪一部分會擁有某個特定的事件?要明白這個問題問的是什麼,可以想像畫在一張紙上的一組同心圓。如果你把手指放在圓心上,那麼你的手指指向的不是一個圓,而是紙上的所有圓。也就是說如果點選了頁面的某個按鈕,同時也點選了按鈕的容器元素,甚至點選了整個頁面。不過呢,IE提出的是冒泡流,而網景提出的是捕獲流。
範例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>事件流</title> <style type="text/css"> #content { width: 150px; height: 150px; background-color: red; } #btn { width: 80px; height: 80px; background-color: green; } </style> </head> <body> <p id="content">content <p id="btn">button</p> </p> <script type="text/javascript"> var content = document.getElementById("content"); var btn = document.getElementById('btn'); btn.onclick = function () { alert("btn"); }; content.onclick = function () { alert("content"); }; document.onclick = function () { alert("document"); } </script> </body> </html>
如果點選容器#btn,則彈出的順序是:btn-content-document;如果點擊的是容器#content,則彈出的是content-document;如果點選的是document,彈出的是document。
由此可以看出JavaScript的事件流機制
前面說過,IE提出的是冒泡流,而網景提出的是捕獲流,後來在W3C組織的統一之下,JS支援了冒泡流和捕獲流,但是目前低版的IE瀏覽器還是只能支援冒泡流(IE6,IE7,IE8都只支援冒泡流),所以為了能夠相容於更多的瀏覽器,建議大家使用冒泡流。
JS事件流原理圖如下:
由此可以知道
1、一個完整的JS事件流是從window開始,最後回到window的一個過程
2、事件流被分成三個階段(1~5)捕獲過程、(5~6)目標過程、(6~10)冒泡過程
範例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <style type="text/css"> #wrapp, #innerP, #textSpan { margin: 5px; padding: 5px; box-sizing: border-box; cursor: default; } #wrapp { width: 300px; height: 300px; border: indianred 3px solid; } #innerP { width: 200px; height: 200px; border: hotpink 3px solid; } #textSpan { display: block; width: 100px; height: 100px; border: orange 3px solid; } </style> </head> <body> <p id="wrapp">wrapp <p id="innerP">innerP <span id="textSpan">textSpan</span> </p> </p> <script> var wrapp = document.getElementById("wrapp"); var innerP = document.getElementById("innerP"); var textSpan = document.getElementById("textSpan"); // 捕获阶段绑定事件 window.addEventListener("click", function (e) { console.log("window 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); document.addEventListener("click", function (e) { console.log("document 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); document.documentElement.addEventListener("click", function (e) { console.log("documentElement 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); document.body.addEventListener("click", function (e) { console.log("body 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); wrapp.addEventListener("click", function (e) { console.log("wrapp 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); innerP.addEventListener("click", function (e) { console.log("innerP 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); textSpan.addEventListener("click", function (e) { console.log("textSpan 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); // 冒泡阶段绑定的事件 window.addEventListener("click", function (e) { console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false); document.addEventListener("click", function (e) { console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false); document.documentElement.addEventListener("click", function (e) { console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false); document.body.addEventListener("click", function (e) { console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false); wrapp.addEventListener("click", function (e) { console.log("wrapp 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false); innerP.addEventListener("click", function (e) { console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false); textSpan.addEventListener("click", function (e) { console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false);</script> </body> </html>
這個時候,如果點選textSpan這個元素,控制台會印出這樣的內容:
##從上面所畫的事件傳播的過程能夠看出來,當點擊滑鼠後,會先發生事件的捕獲1· 捕獲階段:首先
window會被捕獲到事件,之後document、documentElement、body 會捕捉到,再之後就是在bodyDOM元素一層一層的捕獲到事件,有wrapp、innerP。
2· 目標階段:真正點擊的元素textSpan的事件發生了兩次,因為在上面的JavaScript程式碼中,textSapn既在捕獲階段綁定了事件,又在冒泡階段綁定了事件,所以發生了兩次。但這裡有一點是需要注意,在目標階段並不一定先發生在捕獲階段所綁定的事件,而是先綁定的事件發生,一會會解釋一下。
3· 冒泡階段:會和捕獲階段相反的步驟將事件一步一步的冒泡到window
上述程式碼中的兩個屬性:e.target和e.currentTarget
target和currentTarget都是event上面的属性,target是真正发生事件的DOM元素,而currentTarget是当前事件发生在哪个DOM元素上。
可以结合控制台打印出来的信息理解下,目标阶段也就是 target == currentTarget的时候。我没有打印它们两个因为太长了,所以打印了它们的nodeName,但是由于window没有nodeName这个属性,所以是undefined。
那可能有一个疑问,我们不用addEventListener绑定的事件会发生在哪个阶段呢,我们来一个测试,顺便再演示一下我在上面的目标阶段所说的目标阶段并不一定先发生捕获阶段所绑定的事件是怎么一回事。
<script> var wrapp = document.getElementById("wrapp"); var innerP = document.getElementById("innerP"); var textSpan = document.getElementById("textSpan"); // 测试直接绑定的事件到底发生在哪个阶段 wrapp.onclick = function () { console.log("wrapp onclick 测试直接绑定的事件到底发生在哪个阶段") }; // 捕获阶段绑定事件 window.addEventListener("click", function (e) { console.log("window 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); document.addEventListener("click", function (e) { console.log("document 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); document.documentElement.addEventListener("click", function (e) { console.log("documentElement 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); document.body.addEventListener("click", function (e) { console.log("body 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); wrapp.addEventListener("click", function (e) { console.log("wrapp 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); innerP.addEventListener("click", function (e) { console.log("innerP 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); textSpan.addEventListener("click", function () { console.log("textSpan 冒泡 在捕获之前绑定的") }, false); textSpan.onclick = function () { console.log("textSpan onclick") }; textSpan.addEventListener("click", function (e) { console.log("textSpan 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); // 冒泡阶段绑定的事件 window.addEventListener("click", function (e) { console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false); document.addEventListener("click", function (e) { console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false); document.documentElement.addEventListener("click", function (e) { console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false); document.body.addEventListener("click", function (e) { console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false); wrapp.addEventListener("click", function (e) { console.log("wrapp 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false); innerP.addEventListener("click", function (e) { console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false); textSpan.addEventListener("click", function (e) { console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false);</script>
控制台打印如下:
1· textSpan是被点击的元素,也就是目标元素,所有在textSpan上绑定的事件都会发生在目标阶段,在绑定捕获代码之前写了绑定的冒泡阶段的代码,所以在目标元素上就不会遵守先发生捕获后发生冒泡这一规则,而是先绑定的事件先发生。[在目标元素上就不会遵守先发生捕获后发生冒泡这一规则,而是先绑定的事件先发生]
2· 由于wrapp不是目标元素,所以它上面绑定的事件会遵守先发生捕获后发生冒泡的规则。所以很明显用onclick直接绑定的事件发生在了冒泡阶段。
说一下事件绑定、解绑还有阻止事件默认行为:
事件绑定:
1、直接获取元素绑定:
element.onclick = function(e){ // ... };
该方法的优点是:简单和稳定,可以确保它在你使用的不同浏览器中运作一致;处理事件时,this关键字引用的是当前元素,这很有帮组。
缺点:只会在事件冒泡中运行;一个元素一次只能绑定一个事件处理函数,新绑定的事件处理函数会覆盖旧的事件处理函数;事件对象参数(e)仅非IE浏览器可用
2、直接在元素里面使用事件属性
3、W3C方法:
element.onclick = function(e){ // ... };
优点:该方法同时支持事件处理的捕获和冒泡阶段;事件阶段取决于addEventListener最后的参数设置:false (冒泡) 或 true (捕获);在事件处理函数内部,this关键字引用当前元素;事件对象总是可以通过处理函数的第一个参数(e)捕获;可以为同一个元素绑定你所希望的多个事件,同时并不会覆盖先前绑定的事件
缺点:IE不支持,你必须使用IE的attachEvent函数替代。
IE下的方法:
element.attachEvent('onclick', function(){ // ... });
优点:可以为同一个元素绑定你所希望的多个事件,同时并不会覆盖先前绑定的事件。
缺点:IE仅支持事件捕获的冒泡阶段;事件监听函数内的this关键字指向了window对象,而不是当前元素(IE的一个巨大缺点);事件对象仅存在与window.event参数中;事件必须以ontype的形式命名,比如,onclick而非click;仅IE可用,你必须在非IE浏览器中使用W3C的addEventListener
注意:不是意味这低版本的ie没有事件捕获,它也是先发生事件捕获,再发生事件冒泡,只不过这个过程无法通过程序控制。
解除事件:
element.removeEventListener('click', function(e){ // ... }, false);
IE:
element.detachEvent('onclick', function(){ // ... });
阻止事件传播
在支持addEventListener()的浏览器中,可以调用事件对象的stopPropagation()方法以阻止事件的继续传播。如果在同一对象上定义了其他处理程序,剩下的处理程序将依旧被调用,但调用stopPropagation()之后任何其他对象上的事件处理程序将不会被调用。不仅可以阻止事件在冒泡阶段的传播,还能阻止事件在捕获阶段的传播。
IE9之前的IE不支持stopPropagation()方法,而是设置事件对象cancelBubble属性为true来实现阻止事件进一步传播。
<script> var wrapp = document.getElementById("wrapp"); var innerP = document.getElementById("innerP"); var textSpan = document.getElementById("textSpan"); // 测试直接绑定的事件到底发生在哪个阶段 wrapp.onclick = function () { console.log("wrapp onclick 测试直接绑定的事件到底发生在哪个阶段") }; // 捕获阶段绑定事件 window.addEventListener("click", function (e) { console.log("window 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); document.addEventListener("click", function (e) { console.log("document 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); document.documentElement.addEventListener("click", function (e) { console.log("documentElement 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); document.body.addEventListener("click", function (e) { console.log("body 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); wrapp.addEventListener("click", function (e) { console.log("wrapp 捕获", e.target.nodeName, e.currentTarget.nodeName); // 在捕获阶段阻止事件的传播 e.stopPropagation(); }, true); innerP.addEventListener("click", function (e) { console.log("innerP 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); textSpan.addEventListener("click", function () { console.log("textSpan 冒泡 在捕获之前绑定的") }, false); textSpan.onclick = function () { console.log("textSpan onclick") }; textSpan.addEventListener("click", function (e) { console.log("textSpan 捕获", e.target.nodeName, e.currentTarget.nodeName); }, true); // 冒泡阶段绑定的事件 window.addEventListener("click", function (e) { console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false); document.addEventListener("click", function (e) { console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false); document.documentElement.addEventListener("click", function (e) { console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false); document.body.addEventListener("click", function (e) { console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false); wrapp.addEventListener("click", function (e) { console.log("wrapp 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false); innerP.addEventListener("click", function (e) { console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false); textSpan.addEventListener("click", function (e) { console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName); }, false);</script>
实际上我们点击的是textSpan,但是由于在捕获阶段事件就被阻止了传播,所以在textSpan上绑定的事件根本就没有发生,冒泡阶段绑定的事件自然也不会发生,因为阻止事件在捕获阶段传播的特性,e.stopPropagation()很少用到在捕获阶段去阻止事件的传播,大家就以为e.stopPropagation()只能阻止事件在冒泡阶段传播。
阻止事件的默认行为
e.preventDefault()可以阻止事件的默认行为发生,默认行为是指:点击a标签就转跳到其他页面、拖拽一个图片到浏览器会自动打开、点击表单的提交按钮会提交表单等等,因为有的时候我们并不希望发生这些事情,所以需要阻止默认行为。
IE9之前的IE中,可以通过设置事件对象的returnValue属性为false达到同样的效果。
(function(){ var color_list = document.getElementById('color-list'); var colors = color_list.getElementsByTagName('li'); for(var i=0;i<colors.length;i++){ colors[i].addEventListener('click',showColor,false); }; function showColor(e){ var x = e.target; alert("The color is " + x.innerHTML); }; })();
事件委托:
在JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。导致这一问题的原因是多方面的。首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的DOM访问次数,会延迟整个页面的交互就绪时间。
对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click事件会一直冒泡到document层次。也就是说,我们可以为整个页面指定一个onclick事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。
<ul id="color-list"> <li>red</li> <li>yellow</li> <li>blue</li> <li>green</li> <li>black</li> <li>white</li> </ul>
如果点击页面中的li元素,然后输出li当中的颜色,我们通常会这样写:
(function(){ var color_list = document.getElementById('color-list'); var colors = color_list.getElementsByTagName('li'); for(var i=0;i<colors.length;i++){ colors[i].addEventListener('click',showColor,false); }; function showColor(e){ var x = e.target; alert("The color is " + x.innerHTML); }; })();
利用事件流的特性,我们只绑定一个事件处理函数也可以完成:
(function(){ var color_list = document.getElementById('color-list'); color_list.addEventListener('click',showColor,false); function showColor(e){ var x = e.target; if(x.nodeName.toLowerCase() === 'li'){ alert('The color is ' + x.innerHTML); } } })();
冒泡还是捕获?
对于事件代理来说,在事件捕获或者事件冒泡阶段处理并没有明显的优劣之分,但是由于事件冒泡的事件流模型被所有主流的浏览器兼容,从兼容性角度来说还是建议大家使用事件冒泡模型。
事件委托还有一个好处就是添加进来的元素也能绑定事件:
没有使用事件委托:
<body> <ul id="thl"> <li>001</li> <li>002</li> <li>003</li> </ul> <button onclick="fun()">touch</button> <script> var thl = document.getElementById('thl'); var aLi = thl.getElementsByTagName('li'); for (var i = 0; i < aLi.length; i++) { aLi[i].onclick = fn; } function fn() { console.log(this.innerHTML); } function fun() { var node = document.createElement("li"); var textnode = document.createTextNode("maomaoliang"); node.appendChild(textnode); document.getElementById("thl").appendChild(node); }</script> </body>
使用了事件委托:
<script> var thl = document.getElementById('thl'); thl.onclick = function (ev) { ev = ev || event; //兼容处理 var target = ev.target || ev.srcElement; //找到li元素 if (target.nodeName.toLowerCase() == 'li') { console.log(target.innerHTML); } }; function fun() { var node = document.createElement("li"); var textnode = document.createTextNode("maomaoliang"); node.appendChild(textnode); document.getElementById("thl").appendChild(node); }</script>
以上就是本篇文章的内容,大家对JavaScript的不太熟悉的可以多看看哦!
相关推荐:
以上是細說什麼是js中的事件冒泡和事件捕獲以及事件委託的詳細內容。更多資訊請關注PHP中文網其他相關文章!