【事件传播】
事件冒泡
事件冒泡概念 :
当一个元素的事件被触发时,同样的事件将会在该元素的所有祖先元素中依次被触发。这一过程被称为事件冒泡
简单理解:当一个元素触发事件后,会依次向上调用所有父级元素的同名事件
事件冒泡是默认存在的
鼠标经过事件:
mouseover
和 mouseout
会有冒泡效果
mouseenter
和 mouseleave
没有冒泡效果(推荐)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 <style type ="text/css" > #box1 { width : 200px ; height : 200px ; background-color : yellowgreen; } #s1 { background-color : yellow; } </style > <script type ="text/javascript" > window .onload = function ( ){ var s1 = document .getElementById ("s1" ); s1.onclick = function (event ){ event = event || window .event ; alert ("我是span的单击响应函数" ); event.cancelBubble = true ; }; var box1 = document .getElementById ("box1" ); box1.onclick = function (event ){ event = event || window .event ; alert ("我是div的单击响应函数" ); }; document .body .onclick = function ( ){ alert ("我是body的单击响应函数" ); }; }; </script > <body > <div id ="box1" > 我是box1 <span id ="s1" > 我是span</span > </div >
阻止冒泡
阻止冒泡是指阻断事件的流动,保证事件只在当前元素被执行,而不再去影响到其对应的祖先元素。
stopPropagation
方法阻止事件在 DOM 中继续传播,防止再触发定义在别的节点上的监听函数,但是不包括在当前节点上其他的事件监听函数。
1 2 3 4 5 function stopEvent (e ) { e.stopPropagation (); } el.addEventListener ('click' , stopEvent, false );
上面代码中,click
事件将不会进一步冒泡到el
节点的父节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <body > <h3 > 阻止冒泡</h3 > <p > 阻止冒泡是指阻断事件的流动,保证事件只在当前元素被执行,而不再去影响到其对应的祖先元素。</p > <div class ="outer" > <div class ="inner" > <div class ="child" > </div > </div > </div > <script > const outer = document .querySelector ('.outer' ) const inner = document .querySelector ('.inner' ) const child = document .querySelector ('.child' ) outer.addEventListener ('click' , function ( ) { console .log ('outer...' ) }) console .log ('inner...' ) ev.stopPropagation () }) child.addEventListener ('click' , function (ev ) { console .log ('child...' ) ev.stopPropagation () }) </script > </body >
结论:事件对象中的 e.stopPropagation
方法,专门用来阻止事件冒泡。
阻止默认事件
e.preventDefault()
方法用来阻止事件产生的 “默认动作”。
一些特殊的业务需求,需要阻止事件的 “默认动作”。
【小案例1】
制作一个文本框,只能让用户在其中输入小写字母和数字,其他字符输入没有效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > <p > 只能输入小写字母和数字: <input type ="text" id ="field" > </p > <script > var oField = document .getElementById ('field' ); oField.onkeypress = function (e ) { console .log (e.charCode ); if (!(e.charCode >= 48 && e.charCode <= 57 || e.charCode >= 97 && e.charCode <= 122 )) { e.preventDefault (); } }; </script > </body > </html >
style=“zoom: 33%;” />
【小案例2】
制作鼠标滚轮事件:当鼠标在盒子中向下滚动时,数字加 1;反之,数字减 1。
鼠标滚轮事件是 onwheel
,它的事件对象 e 提供 deltaY
属性表示鼠标滚动方向,向下滚动是返回正值,向上滚动时返回负值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > <style > #box { width : 200px ; height : 200px ; background-color : #333 ; } body { height : 2000px ; } </style > </head > <body > <div id ="box" > </div > <h1 id ="info" > 0</h1 > <script > var oBox = document .getElementById ('box' ); var oInfo = document .getElementById ('info' ); var a = 0 ; oBox.onwheel = function (e ) { if (e.deltaY > 0 ) { a++; } else { a--; } oInfo.innerText = a; } </script > </body > </html >
style=“zoom: 33%;” />
1 2 3 4 5 6 7 8 9 10 11 12 oBox.onmousewheel = function (e ) { e.preventDefault (); if (e.deltaY > 0 ) { a++; } else { a--; } oInfo.innerText = a; }
style=“zoom:33%;” />
事件委托
批量添加事件监听
题目:页面上有一个无序列表 <ul>
,它内部共有 20 个 <li>
元素,请批量给它们添加事件监听,实现效果:点击哪个 <li>
元素,哪个 <li>
元素就变红。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > <ul id ="list" > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > </ul > <script > var oList = document .getElementById ('list' ); var lis = oList.getElementsByTagName ('li' ); for (var i = 0 ; i < lis.length ; i++) { lis[i].onclick = function ( ) { this .style .color = 'red' ; }; } </script > </body > </html >
批量添加事件监听的性能问题
每一个事件监听注册都会消耗一定的系统内存,而批量添加事件会导致监听数量太多,内存消耗会非常大。
实际上,每个 <li>
的事件处理函数都是不同的函数,这些函数本身也会占用内存。
新增元素动态绑定事件
题目:页面上有一个无序列表 <ul>
,它内部没有 <li>
元素,请制作一个按钮,点击这个按钮就能增加一个 <li>
元素。并且要求每个增加的 <li>
元素也要有点击事件监听,实现效果:点击哪个 <li>
元素,哪个 <li>
元素就变红。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > <button id ="btn" > 按我添加新的li列表项</button > <ul id ="list" > </ul > <script > var oBtn = document .getElementById ('btn' ); var oList = document .getElementById ('list' ); var lis = oList.getElementsByTagName ('li' ); oBtn.onclick = function ( ) { var oLi = document .createElement ('li' ); oLi.innerHTML = '我是列表项' ; oList.appendChild (oLi); oLi.onclick = function ( ) { this .style .color = 'red' ; }; }; </script > </body > </html >
动态绑定事件的问题
新增元素必须分别添加事件监听,不能自动获得事件监听。
大量事件监听、大量事件处理函数都会产生大量的内存消耗。
事件委托
利用事件冒泡机制,将后代元素事件委托给祖先元素。
由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的委托(delegation)。
e.target和e.currentTarget属性
事件委托通常需要结合使用 e.target 属性。
属性
属性描述
target
触发此事件的最早元素,即 “事件源元素”
currentTarget
事件处理程序附加到的元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > <button id ="btn" > 按我创建一个新列表项</button > <ul id ="list" > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > </ul > <script > var oList = document .getElementById ('list' ); var oBtn = document .getElementById ('btn' ); oList.onclick = function (e ) { e.target .style .color = 'red' ; }; oBtn.onclick = function ( ) { var oLi = document .createElement ('li' ); oLi.innerText = '我是新来的' ; oList.appendChild (oLi); }; </script > </body > </html >
style=“zoom: 67%;” />
事件发生以后,会经过捕获和冒泡两个阶段,依次通过多个 DOM 节点。因此,任意事件都有两个与事件相关的节点,一个是事件的原始触发节点(Event.target
),另一个是事件当前正在通过的节点(Event.currentTarget
)。前者通常是后者的后代节点。
Event.currentTarget
属性返回事件当前所在的节点,即事件当前正在通过的节点,也就是当前正在执行的监听函数所在的那个节点。随着事件的传播,这个属性的值会变。
Event.target
属性返回原始触发事件的那个节点,即事件最初发生的节点。这个属性不会随着事件的传播而改变。
事件传播过程中,不同节点的监听函数内部的Event.target
与Event.currentTarget
属性的值是不一样的。
1 2 3 4 5 6 7 8 9 10 11 12 function hide (e ) { console .log (this === e.currentTarget ); console .log (this === e.target ); } document .getElementById ('para' ).addEventListener ('click' , hide, false );
上面代码中,<em>
是<p>
的子节点,点击<em>
或者点击<p>
,都会导致监听函数执行。这时,e.target
总是指向原始点击位置的那个节点,而e.currentTarget
指向事件传播过程中正在经过的那个节点。由于监听函数只有事件经过时才会触发,所以e.currentTarget
总是等同于监听函数内部的this
。
使用事件委托时需要注意的事项
(1)onmouseenter
和 onmouseover
都表示 “鼠标进入”,它们有什么区别呢?
答:onmouseenter
不冒泡,onmouseover
冒泡。
使用事件委托时要注意:不能委托不冒泡的事件给祖先元素。
【onmouseenter】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > <button id ="btn" > 按我创建一个新列表项</button > <ul id ="list" > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > </ul > <script > var oList = document .getElementById ('list' ); var oBtn = document .getElementById ('btn' ); oList.onmouseenter = function (e ) { e.target .style .color = 'red' ; }; oBtn.onclick = function ( ) { var oLi = document .createElement ('li' ); oLi.innerText = '我是新来的' ; oList.appendChild (oLi); }; </script > </body > </html >
style=“zoom:67%;” />
【onmouseover】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > <button id ="btn" > 按我创建一个新列表项</button > <ul id ="list" > <li > 列表项</li > <li > 列表项</li > <li > 列表项</li > </ul > <script > var oList = document .getElementById ('list' ); var oBtn = document .getElementById ('btn' ); oList.onmouseover = function (e ) { e.target .style .color = 'red' ; }; oBtn.onclick = function ( ) { var oLi = document .createElement ('li' ); oLi.innerText = '我是新来的' ; oList.appendChild (oLi); }; </script > </body > </html >
style=“zoom:67%;” />
(2)最内层元素不能再有额外的内层元素了,比如:
这会导致点击 li 时效果正常,但是点击 span 时,只有 span 会变色。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > <button id ="btn" > 按我创建一个新列表项</button > <ul id ="list" > <li > <span > 我是span</span > 列表项</li > <li > <span > 我是span</span > 列表项</li > <li > <span > 我是span</span > 列表项</li > <li > <span > 我是span</span > 列表项</li > <li > <span > 我是span</span > 列表项</li > </ul > <script > var oList = document .getElementById ('list' ); var oBtn = document .getElementById ('btn' ); oList.onmouseover = function (e ) { e.target .style .color = 'red' ; }; oBtn.onclick = function ( ) { var oLi = document .createElement ('li' ); oLi.innerText = '我是新来的' ; oList.appendChild (oLi); }; </script > </body > </html >
style=“zoom:67%;” />
事件流
小案例引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > <style > #box1 { width : 202px ; height : 202px ; border : 1px solid #000 ; padding : 50px ; } #box2 { width : 100px ; height : 100px ; border : 1px solid #000 ; padding : 50px ; } #box3 { width : 100px ; height : 100px ; border : 1px solid #000 ; } </style > </head > <body > <div id ="box1" > <div id ="box2" > <div id ="box3" > </div > </div > </div > <script > var oBox1 = document .getElementById ('box1' ); var oBox2 = document .getElementById ('box2' ); var oBox3 = document .getElementById ('box3' ); oBox2.onclick = function ( ) { console .log ('我是 box2 的 onclick' ); }; oBox3.onclick = function ( ) { console .log ('我是 box3 的 onclick' ); }; oBox1.onclick = function ( ) { console .log ('我是 box1 的 onclick' ); }; </script > </body > </html >
style=“zoom:50%;” />
捕获阶段和冒泡阶段
一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。
第一阶段 :从window
对象传导到目标节点(上层传到底层),称为“捕获阶段”(capture phase)。
第二阶段 :在目标节点上触发,称为“目标阶段”(target phase)。
第三阶段 :从目标节点传导回window
对象(从底层传回上层),称为“冒泡阶段”(bubbling phase)。
这种三阶段的传播模型,使得同一个事件会在多个节点上触发。
addEventListener()的第三个参数
onxxx写法只能监听冒泡阶段
addEventListener()方法
1 oBox.addEventListener ('click' , function ( ){}, true );
Event:事件
上面代码中,<div>
节点之中有一个<p>
节点。
如果对这两个节点,都设置click
事件的监听函数(每个节点的捕获阶段和冒泡阶段,各设置一个监听函数),共计设置四个监听函数。然后,对<p>
点击,click
事件会触发四次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 var phases = { 1 : 'capture' , 2 : 'target' , 3 : 'bubble' }; var div = document .querySelector ('div' );var p = document .querySelector ('p' );div.addEventListener ('click' , callback, true ); p.addEventListener ('click' , callback, true ); div.addEventListener ('click' , callback, false ); p.addEventListener ('click' , callback, false ); function callback (event ) { var tag = event.currentTarget .tagName ; var phase = phases[event.eventPhase ]; console .log ("Tag: '" + tag + "'. EventPhase: '" + phase + "'" ); }
上面代码表示,click
事件被触发了四次:<div>
节点的捕获阶段和冒泡阶段各1次,<p>
节点的目标阶段触发了2次。
捕获阶段:事件从<div>
向<p>
传播时,触发<div>
的click
事件;
目标阶段:事件从<div>
到达<p>
时,触发<p>
的click
事件;
冒泡阶段:事件从<p>
传回<div>
时,再次触发<div>
的click
事件。
其中,<p>
节点有两个监听函数(addEventListener
方法第三个参数的不同,会导致绑定两个监听函数),因此它们都会因为click
事件触发一次。所以,<p>
会在target
阶段有两次输出。
注意,浏览器总是假定click
事件的目标节点,就是点击位置嵌套最深的那个节点(本例是<div>
节点里面的<p>
节点)。所以,<p>
节点的捕获阶段和冒泡阶段,都会显示为target
阶段。
事件传播的最上层对象是window
,接着依次是document
,html
(document.documentElement
)和body
(document.body
)。也就是说,上例的事件传播顺序,在捕获阶段依次为window
、document
、html
、body
、div
、p
,在冒泡阶段依次为p
、div
、body
、html
、document
、window
。
结论:
addEventListener
第3个参数决定了事件是在捕获阶段触发还是在冒泡阶段触发
addEventListener
第3个参数为 true
表示捕获阶段触发,false
表示冒泡阶段触发,默认值为 false
事件流只会在父子元素具有相同事件类型时才会产生影响
绝大部分场景都采用默认的冒泡模式(其中一个原因是早期 IE 不支持捕获)
removeEventListener()方法
当我们 addEventListener() 后,该监听事件就会一直生效,直到关闭页面或是移除该对应的监听!
removeEventListener() 方法用来移除监听事件(只能移除具名函数的监听,且方法名称后面不能带 ()
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 var body = document .querySelector ('body' ),var clickTarget = document .getElementById ('click-target' ),var mouseOverTarget = document .getElementById ('mouse-over-target' ),var toggle = false ;function makeBackgroundYellow ( ) { 'use strict' ; if (toggle) { body.style .backgroundColor = 'white' ; } else { body.style .backgroundColor = 'yellow' ; } toggle = !toggle; } clickTarget.addEventListener ('click' , makeBackgroundYellow, false ); mouseOverTarget.addEventListener ('mouseover' , function ( ) { 'use strict' ; clickTarget.removeEventListener ('click' , makeBackgroundYellow, false ); });
特别注意
addEventListener() 一但注册某个事件,那么这个事件是会一直生效的,就算是该注册事件写在某个函数中,那个函数调用已经结束了,但是该事件还是会存在!因为事件的注册是直接绑定到相应的元素上的,并且是异步的,除非页面被关闭,或者是移除该监听!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > <button id ="btn" > 点击</button > <script > var btn = document .getElementById ('btn' ); function demo ( ) { console .log ('btn' ); } function test ( ) { btn.addEventListener ('click' , demo, false ); return 'over' ; } console .log (test ()); </script > </body > </html > 执行结果: over (点击按钮) btn
三大家族scroll、offset、client
定时器和延时器