第七章 DOM事件

67 阅读16分钟

基础概念

  • 事件源(事件目标)

    事件目标阶段中的元素

    事件目标阶段中只会存在一个元素,该元素就是事件源

  • 事件捕获

    结构上存在嵌套关系的元素,存在事件捕获,即父元素能捕获到在子元素身上触发的事件

    捕获阶段的事件触发顺序是先触发父,再触发子

  • 事件冒泡

    结构上存在嵌套关系的元素,存在事件冒泡,即在子元素身上触发的事件会冒泡到父元素上

    冒泡阶段的事件触发顺序是先触发子,再触发父

注意:

  • document对象虽不是一个具体元素,但它也能注册和触发事件,也会参与到事件捕获和事件冒泡两个阶段中
  • window对象也能触发和注册事件,也参与事件冒泡和事件捕获两个阶段,且它是作为事件传播链的最顶端出现(事件捕获阶段最先触发事件的就是window对象,其次是document对象)
  • 注册的事件默认是以事件冒泡的顺序触发的

以下面的结构为例,当点击button元素后,事件传播的过程为:

<!DOCTYPE html>
<html>
    <body>
        <div>
        	<button>click</button>
        </div>
    </body>
</html>

image.png

事件注册

事件注册,即给元素的某个事件绑定事件处理程序

注册的方式:

  • <div onclick="console.log('div click')"></div>

  • dom.onclick = function(){}

    这种方式是将事件处理函数作为属性的值复制给对应的属性,因此最多只能为元素绑定一个事件处理函数,因为多次给同一个属性赋值会发生覆盖

    当dom是事件源时,事件处理函数将会在事件目标阶段执行;当dom不是事件源时,事件处理函数将会在事件冒泡阶段执行

  • dom.addEventListener("click", function(){})

    该方式可以为一个元素的同一事件绑定多个事件处理函数

    同一个元素的同一事件的多个处理函数,若执行的阶段相同,则按照注册的先后顺序(源代码顺序)依次执行

    该方法可以传入第三个参数,传入false,表示当dom不是事件源时,绑定的事件处理函数会在冒泡阶段执行;传入true,表示当dom不是事件源时,绑定的事件处理函数会在捕获阶段执行。其默认为false

    当dom是事件源时,所有事件处理函数始终会在事件目标阶段执行,此时第三个参数无意义

注意:事件并不是只有绑定了处理函数后才能被触发,即使不绑定事件处理函数,事件也照样能够触发,并且也存在事件的三个阶段

扩展

addEventListener的第三个参数可以传入一个配置对象,用于控制当前注册的事件处理函数的执行:

  • capture:是否在捕获阶段运行(仅在非事件目标阶段运行时才有意义),取值为布尔值,默认为false
  • once:事件处理函数是否只运行一次,取值为布尔值,默认为false

取消注册

  • dom.onclick = null

    dom.onclick = undefined

  • dom.removeEventListener("click", 函数引用)

    第二个参数传入的是要删除的函数的引用

    该方法可以传入第三个参数,其默认值为false。当使用该方法移除某个事件处理函数时,第三个参数也需要与注册时对应,否则也将移除失败

    <!DOCTYPE html>
    <div id="div" style="height: 100px;background: lightblue;"></div>
    
    <script>
        function handler(){
            console.log("div click");
        }
    
        div.addEventListener("click", handler, {
        	capture: true
        });
        
        div.removeEventListener("click", handler, false);		// 移除失败
        div.removeEventListener("click", handler, true);		// 移除成功
    </script>
    

addEventListener和removeEventListener方法在IE8以下不兼容,它们提供的方法叫做attachEvent和detachEvent,两者用法一样

事件对象

事件对象中包含了事件发生时的相关信息

不同类型的事件触发,得到的事件对象不同

获取事件对象

  • <div onclick="concole.log(event)"></div>

    在元素的onclick属性中书写的代码,相当于是在某个函数环境中,浏览器会给该函数提供了一个叫做event的局部变量,该变量就是事件对象

  • 作为事件处理函数的参数

    在事件触发时,浏览器自动调用相应的事件处理函数,并将事件对象作为实参传入

  • 旧版本IE浏览器:通过window.event获取

    兼容写法:

    div.onclick = function(e){
        e = e || window.event;
    }
    

事件对象中的通用成员

不同类型事件对应的事件对象不同,但它们都具有下面的成员

  • target & srcElement

    用于获取事件源元素,前者是现代标准中的写法,后者只有IE8以下支持

    兼容写法:

    div.onclick = function(e){
        e = e || window.event;
        target = e.target || e.srcElement;
    }
    

    注意:并不是一定要经过事件目标阶段后才能明确事件源元素,事件源的父元素在捕获阶段触发的事件仍能获取到准确的事件源元素

    <!DOCTYPE html>
    <html>
        <head>
            <style>
                #father{
                    width: 100px;
                    height: 100px;
                    background-color: lightblue;
                }
                
                #son{
                    width: 50px;
                    height: 50px;
                    background-color: pink;
                }
            </style>
        </head>
        <body>
            <div id="father">
                <div id="son"></div>
            </div>
            <script>
            	father.addEventListener("click", function(e){
                    console.log(e.target);
                }, true);
                // 点击son所覆盖的区域,会发现father的click事件中打印的仍是son元素
            </script>
        </body>
    </html>
    
事件委托

利用事件源与事件传播的机制,可以实现事件委托

事件委托的具体做法是在祖先元素上注册事件,并通过事件源来确定到底是哪个后代元素触发的事件,当事件源是预期的某个后代元素时,就做一些事情,否则就直接结束事件处理函数

利用事件委托可以减少事件注册的次数,并且事件委托还能自动适应后代元素的数量变化

注意:事件委托并不是只能依赖于事件冒泡,事件捕获阶段也能实现事件委托,因为事件捕获阶段也能获取到具体的事件源元素


  • currentTarget

    注册事件的元素

  • type

    是一个字符串,表示当前事件的类型

  • preventDefault & returnValue

    用于阻止事件的默认行为,前者是一个方法,是现代标准中的写法,后者是一个属性,只有IE8以下浏览器支持

    浏览器会给某些元素的某些事件加入默认的行为,如a元素click后自动跳转页面、form元素submit后自动发送表单数据到服务器并刷新页面等

    在事件处理函数中调用preventDefault()方法即可阻止事件默认行为,returnValue则是设置其属性值为false时即可阻止事件默认行为

    div.onclick = function(e){
        e.preventDefault();
        e.returnValue = false;
    }
    

    远古的阻止事件默认行为的做法:

    使用onxxx注册的事件中,在事件处理处理函数内return false即可阻止事件的默认行为

    <!DOCTYPE html>
    <div onclick="return false"></div>
    
    <script>
    div.onclick = funcion(){
      return false;
    }`
    </script>
    

    注意:必须是使用on的方式注册的事件,必须是返回的布尔false,返回undefined、null、0、""、NaN不算

  • stopPropagation

    一个方法,用于阻止事件冒泡

    调用该方法后,事件将不再冒泡给父元素

  • eventPhase

    得到目前事件处理函数执行时所处的阶段,得到一个数字

    1:事件捕获阶段

    2:事件目标阶段

    3:事件冒泡阶段

  • bubbles

    对于不会冒泡的事件,该属性返回false,否则返回true

鼠标事件

分类

  • click

    当用户单击主鼠标按钮或在元素聚集状态下按下回车后触发

    点击 = 按下 + 抬起

  • dblclick

    用户双击主鼠标按钮时触发

  • mousedown

    用户按下鼠标任意按键时触发

  • mouseup

    用户抬起鼠标任意按键时触发

    当抬起与单击事件同时存在时,先触发抬起再触发单击

  • mousemove

    鼠标在元素上移动时触发

    注意:该事件触发的频率受浏览器刷新频率影响,刷新地越快,触发的也就越快

  • mouseover

    鼠标进入元素时触发

    mouseover的“进入元素”是指进入到只属于元素本身的区域,其后代元素覆盖的区域不算

    试将鼠标直接移入到蓝色区域中(不要先进入红色区域再进入蓝色区域):

    <!DOCTYPE html>
    <style>
        .wrap{
            width: 200px;
            height: 200px;
            background-color: pink;
        }
    
        .inner{
            width: 100px;
            height: 100px;
            background-color: lightblue;
        }
    </style>
    
    <div class="wrap" id="wrap">
        <div class="inner" id="inner"></div>
    </div>
    
    <script>
        inner.onmouseover = function(e){
            e.stopPropagation();		// 防止子元素冒泡而干扰输出结果
        }
    
        inner.onmouseout = function(e){
            e.stopPropagation();		// 防止子元素冒泡而干扰输出结果
        }
    
        wrap.onmouseover = function(){
            console.log("wrap over");
        }
    
        wrap.onmouseout = function(){
            console.log("wrap out");
        }
    </script>
    
  • mouseout

    鼠标离开元素时触发

    mouseout的“离开元素”是指离开了只属于元素自身的区域,鼠标移动到其后代元素所覆盖的区域中也算离开

    试将鼠标从红色区域移动到蓝色区域:

    <!DOCTYPE html>
    <style>
        .wrap{
            width: 200px;
            height: 200px;
            background-color: pink;
        }
    
        .inner{
            width: 100px;
            height: 100px;
            background-color: lightblue;
        }
    </style>
    
    <div class="wrap" id="wrap">
        <div class="inner" id="inner"></div>
    </div>
    
    <script>
        inner.onmouseover = function(e){
            e.stopPropagation();		// 防止子元素冒泡而干扰输出结果
        }
    
        inner.onmouseout = function(e){
            e.stopPropagation();		// 防止子元素冒泡而干扰输出结果
        }
    
        wrap.onmouseover = function(){
            console.log("wrap over");
        }
    
        wrap.onmouseout = function(){
            console.log("wrap out");
        }
    </script>
    
  • mouseenter

    鼠标进入元素时触发,该事件不会冒泡

    mouseenter的“进入元素”包括进入其自身或其子元素内部,这也是与mouseover的区别之一

    就算直接移入其子元素的区域,事件源对象也仍是绑定enter事件的父元素,而不是该子元素:

    <!DOCTYPE html>
    <style>
        .wrap{
            width: 200px;
            height: 200px;
            background-color: pink;
        }
    
        .inner{
            width: 100px;
            height: 100px;
            background-color: lightblue;
            margin-left: 200px;
        }
    </style>
    
    <div class="wrap" id="wrap">
        <div class="inner" id="inner"></div>
    </div>
    
    <script>
        wrap.onmouseenter = function(e){
            console.log(e.target);		// .wrap
            console.log("wrap enter");
        }
    
        wrap.onmouseleave = function(e){
            console.log(e.target);		// .wrap
            console.log("wrap leave");
        }
    </script>
    
  • mouseleave

    鼠标离开元素时触发,该事件不会冒泡

    mouseleave的“离开元素”是指鼠标不在其自身以及其子元素身上,这也是与mouseout的区别之一

    就算是从子元素区域中离开,事件源对象也仍是绑定leave事件的父元素,而不是该子元素:

    <!DOCTYPE html>
    <style>
        .wrap{
            width: 200px;
            height: 200px;
            background-color: pink;
        }
    
        .inner{
            width: 100px;
            height: 100px;
            background-color: lightblue;
            margin-left: 200px;
        }
    </style>
    
    <div class="wrap" id="wrap">
        <div class="inner" id="inner"></div>
    </div>
    
    <script>
        wrap.onmouseenter = function(e){
            console.log(e.target);		// .wrap
            console.log("wrap enter");
        }
    
        wrap.onmouseleave = function(e){
            console.log(e.target);		// .wrap
            console.log("wrap leave");
        }
    </script>
    
  • wheel

    鼠标滚轮滚动时触发

    该事件的触发不依赖于滚动条滚动

事件对象

所有鼠标类事件的事件对象,都为MouseEvent对象

MouseEvent对象中包含以下成员:

  • altKey

    判断触发鼠标事件时,是否按下了键盘的alt

    同类型的还有ctrlKey、shiftKey

  • button

    得到一个数字,代表触发鼠标按下或抬起事件时,涉及的鼠标按键的类型

    0:鼠标左键

    1:鼠标滚轮

    2:鼠标右键

    不涉及鼠标按下和抬起操作的鼠标事件,该属性始终为0

  • pageX、pageY

    获取鼠标距离页面左上角的横纵坐标

  • clientX、clientY

    获取鼠标距离视口左上角的横纵坐标

    页面是指整个文档所占用的区域(包括因滚动而隐藏的区域),视口是指页面中的可见区域

  • offsetX、offsetY

    获取鼠标距离事件源元素的填充盒的左上角的横纵坐标

  • screenX、screenY

    获取鼠标距离屏幕左上角的横纵坐标

  • x、y

    等同于clientX、clientY

  • movementX、movementY

    获取鼠标与上一次鼠标位置所偏移的距离

    只在鼠标移动事件中有效

    该属性的计算方式:curMouseEvent.movementX=curMouseEvent.screenXprevMouseEvent.screenXcurMouseEvent.movementX = curMouseEvent.screenX - prevMouseEvent.screenX

注意:screenX和screenY,movementX和movementY属性是按照系统原始分辨率进行计算的,因此受系统设置中画面缩放比例的影响,但不受浏览器页面缩放比例的影响,而其他坐标属性则受页面缩放效果的影响

分别测试在不同的页面缩放比例下,将鼠标从div左侧移动到div右侧各类坐标的变化情况:

<!DOCTYPE html>
<style>
    div {
        width: 50%;
        height: 100%;
        background-color: pink;
        position: fixed;
        left: 0;
        top: 0;
    }
</style>

<div></div>

<script>
	document.onmousemove = function(e){
        console.clear();
        console.log(e.screenX);
        console.log(e.pageX);
        console.log(e.clientX);
        console.log(e.offsetX);
    }
</script>

在使用这些坐标时,往往希望坐标受页面缩放的影响

比如实现拖拽效果时,若使用的坐标数据不受页面缩放影响,则会导致页面处于放大状态或缩小状态时,无法让元素始终跟随鼠标(看袁老师写的例子)

键盘事件

分类

  • keydown

    按下键盘上任意按键时触发

  • keypress

    按下键盘上任意一个字符按键时触发

    字符按键是指按下后会在屏幕上打印出字符的按键(包括tab键和回车键)

  • keyup

    抬起键盘上任意按键时触发

注意:当文本框触发keydown或keypress事件时,会触发浏览器的一些默认行为,如按下字符键会将字符输入到文本框中、按下BackSpace或Delete键会删除一个字符等,可以通过阻止事件默认行为来消除这些行为

事件对象

所有键盘类事件的事件对象都为KeyboardEvent

该对象中的成员有:

  • code

    得到按键对应的字符串,且区分按键位置,如按下键盘左侧的alt键会返回AltLeft,右侧则返回AltRight

    按下字母类按键时,得到的固定是Key_的形式,不会区分用户是否开启了大写模式

  • key

    得到按键对应的字符串,不区分按键位置,按下任何位置的alt键固定返回Alt

    按下字母类按键时,会根据用户是否开启大写模式来返回对应的字符,开启时则返回大写字符,关闭时则返回小写字符

  • keyCode、which

    即将弃用的两个属性,得到按键对应的编码

  • altKey

    返回布尔值,表示事件触发时alt是否被按下

  • shiftKey

    返回布尔值,表示事件触发时shift是否被按下

  • ctrlKey

    返回布尔值,表示事件触发时ctrl是否被按下

表单事件

  • focus

    元素聚焦时触发

    该事件不会冒泡

  • blur

    元素失焦时触发

  • submit

    表单提交时触发

    仅在form元素上才会生效,该事件会冒泡

  • change

    内容改变事件

    在input、textarea、select元素上注册该事件才有效果,该事件会冒泡

    对于select元素,当选中项发生改变时会立即出发该事件

    对于可以输入文本的input元素或textarea元素,当本次失去焦点时发现文本内容与上一次失焦时不同后才会触发该事件

    不同类型的input元素触发change事件的时机不同,具体请看change事件 MDN

  • input

    内容改变事件

    在input、textarea、select元素上注册该事件才有效果,该事件会冒泡

    对于select元素,当选中项发生改变时会立即出发该事件(和change一致)

    对于input元素或textarea元素,当内容发生改变时也会立即触发该事件

    注意:按下字符按键后在文本框中打印出字符是键盘事件的默认行为,input事件是在文本内容已经改变了之后才触发

    具体细节:input事件 MDN

页面状态事件

  • load

    资源加载完成后触发

    该事件可以注册在window对象上,也可以注册在链接了外部资源的元素(img、script元素等)上

    对于window的load事件,在页面中所有资源(图片、外部css和外部js、音视频等)都加载完毕后触发

    对于其它元素,在对应的链接资源加载完毕后触发

    图片资源是异步加载的,若没有给img元素手动设置宽高,则图片加载完成之前使用JS获取img元素的宽高时,得到的都是0,可以利用img元素的load事件来解决这个问题

    function getImgSize(img, callback){
           var style = getComputedStyle(img);
           if(style.width === 0 && style.height === 0){
               // 此时img元素还没有加载完成
               img.onload = function(){
                   // 等待img加载完成后调用callback
                   var style = getComputedStyle(img);
                   callback({
                       width: style.width,
                       height: style.height
                   });
               }
           } else {
               // img元素在该函数调用之前就已经加载完成了
               callback({
                   width: style.width,
                   height: style.height
               });
           }
    }
    
    getImgSize(img, function(size){
           console.log(size);
           // 对img进行后续操作...
    });
    

    为避免页面出现闪烁,css代码应该写到文档的顶部

    为避免阻塞后续的渲染,JS代码应该写到页面的底部,同时也避免在执行JS代码时,可能会获取不到页面中的元素

  • DOMContentLoaded

    DOM树构建完成,且所有延迟脚本(defer和module)都加载并执行完毕后触发

    该事件只能注册在document对象上,并且只能使用addEventListener的方式进行注册

  • readystatechange

    页面状态发生变化时触发

    该事件只能注册在document对象上

    页面从刚开始加载到所有资源加载完成一共要经历三个状态:loading、interactive、complete

    loading状态为初始状态(最开始就是页面的状态就是loading),表示页面开始加载,并且DOM树还没有构建完成

    当DOM树构建完成后,就会进入interactive状态,同时会触发DOMContentLoaded事件,并且此时就可以为DOM元素注册事件了

    当页面中所有资源都加载完成后,状态就转变为complete,同时会触发window的load事件

    可以使用document.readystate来查看此时页面的状态信息

  • unload、beforeunload

    关闭页面时触发

    需要注册在window对象上

    beforeunload发生在unload之前,且一些浏览器还允许在beforeunload事件中提示用户是否真的要关闭页面

  • scroll

    滚动条滚动时触发

    该事件会冒泡

  • resize

    页面视口尺寸变化时触发(放大缩小页面也是一种视口尺寸变化,但视口突然出现滚动条不算视口尺寸变化,因为视口滚动条也属于视口的一部分)

    需要注册在window对象上

  • contextmenu

    右键出菜单事件

    该事件的默认行为是出现菜单,可以阻止默认行为

  • copy

    复制内容时触发

    该事件的默认行为是将内容复制到缓冲区,可以阻止默认行为

  • paste

    粘贴内容时触发

    该事件的默认行为是将内容粘贴到相应位置,可以阻止默认行为

  • cut

    剪切内容时触发

    该事件的默认行为是将内容剪切到缓冲区,可以阻止默认行为

事件模拟

可以使用代码的形式触发某个事件:

  • dom.click()

    运行此代码将触发dom的click事件

  • form.submit()

    运行此代码将触发form的submit事件

  • 其它事件使用dom.dispatchEvent(事件对象)的形式模拟