js 事件

事件与事件流

事件介绍

JavaScript 和 HTML 之间的交互是通过当用户或者浏览器操作网页时发生的事件来处理的。页面装载时,是一个事件,用户点击页面上的某个按钮时,也是一个事件。

在早期拨号上网的年代,如果所有的功能都放在服务器端进行处理的话,效率是非常低的。所以 JavaScript 最初被设计出来就是用来解决这些问题的。通过允许一些功能在客户端处理,以节省到服务器的往返时间。

JavaScript 中采用一个叫做事件监听器的东西来监听事件是否发生。这个事件监听器类似于一个通知,当事件发生时,事件监听器会让我们知道,然后程序就可以做出相应的响应。

通过这种方式,就可以避免让程序不断地去检查事件是否发生,让程序在等待事件发生的同时,可以继续做其他的任务。
接下来我们来看一个事件的快速入门案例,如下:

<body>
    <button onclick="test()">点击我</button>
    <script>
        let test = function(){
            alert("我知道你已经点击了");
        }
    </script>
</body>

效果:点击按钮以后弹出一个对话框

事件流介绍

当浏览器发展到第 4 代时(Internet Explorer 4 及 Netscape 4),浏览器开发团队遇到了一个很有意思的问题:页面的哪一部分会拥有某个特定的事件?

想象在一张纸上的一组同心圆。如果把手指放在圆心上,那么手指指向的不是一个圆,而是纸上的所有圆。

好在两家公司的浏览器开发团队在看待浏览器事件方面还是一致的。如果单击了某个按钮,他们都认为单击事件不仅仅发生在按钮上,甚至也单击了整个页面。

但有意思的是,Internet Explorer 和 Netscape 开发团队居然提出了差不多是完全相反的事件流的概念。Internet Explorer 的事件流是事件冒泡流,而 Netscape 的事件流是事件捕获流。

事件冒泡

Internet Explorer 的事件流叫做事件冒泡(event bubbling),即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。

以下列 HTML 结构为例,来说明事件冒泡。如下:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
    </head>
    <body>
        <div></div>
    </body>
</html>

如果单击了页面中的 div 元素,那么这个 click 事件沿 DOM 树向上传播,在每一级节点上都会发生,按照如下顺序进行传播:

1.div
2.body
3.html
4.document

所有现代浏览器都支持事件冒泡,但在具体实现在还是有一些差别。Internet Explorer 9、Firefox、Chrome、Safari 将事件一直冒泡到 window 对象。

1.div
2.body
3.html
4.document
5.window

我们可以通过下面的代码,来查看文档具体的冒泡顺序,示例如下:

<body>
    <div id="box" style="height:100px;width:300px;background-color:pink;"></div>
    <button id="reset">还原</button>
    <script>
        // IE 8 以下浏览器返回 div body html document
        // 其他浏览器返回 div body html document window
        reset.onclick = function () { 
            history.go(); 
        }
        box.onclick = function () { 
            box.innerHTML += 'div\n'; 
        }
        document.body.onclick = function () { 
            box.innerHTML += 'body\n'; 
        }
        document.documentElement.onclick = function () {
            box.innerHTML += 'html\n'; 
        }
        document.onclick = function () { 
            box.innerHTML += 'document\n'; 
        }
        window.onclick = function () { 
            box.innerHTML += 'window\n'; 
        }
    </script>
</body>

事件捕获

Netscape Communicator 团队提出的另一种事件流叫做事件捕获(event captruing)。事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。

事件捕获的思想是在事件到达预定目标之前就捕获它。以同样的 HTML 结构为例来说明事件捕获,如下:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
    </head>
    <body>
        <div></div>    
    </body>
</html>

在事件捕获过程中,document 对象首先接收到 click 事件,然后事件沿 DOM 树依次向下,一直传播到事件的实际目标,即 div 元素:

1.document
2.html
3.body
4.div

Internet Explorer 9、Firefox、Chrome、Safari 等现代浏览器都支持事件捕获,但是也是从 window 对象开始捕获。

1.window
2.document
3.html
4.body
5.div

后面我们会学习addEventListener()这个方法。在这个方法中当第三个参数设置为 true 时,即为事件捕获。我们由此可以来观察一下事件捕获的一个顺序,如下:

<body>
    <div id="box" style="height:100px;width:300px;background-color:pink;"></div>
    <button id="reset">还原</button>
    <script>
        // IE 8 以下浏览器不支持
        // 其他浏览器返回 window document html body div
        reset.onclick = function () { 
            history.go(); 
        }
        box.addEventListener('click', function () { 
            box.innerHTML += 'div\n' 
        }, true)
        document.body.addEventListener('click', function () { 
            box.innerHTML += 'body\n'; 
        }, true);
        document.documentElement.addEventListener('click',function(){
            box.innerHTML+='html\n';
        },true);
        document.addEventListener('click', function () { 
            box.innerHTML += 'document\n'; 
        }, true);
        window.addEventListener('click', function () { 
            box.innerHTML += 'window\n'; 
        }, true);
    </script>
</body>

DOM 事件流

DOM 标准采用捕获 + 冒泡。

两种事件流都会触发 DOM 的所有对象,从 document 对象开始,也在 document 对象结束。换句话说,起点和终点都是 document 对象(很多浏览器可以一直捕获 + 冒泡到 window 对象)

DOM 事件流示意图:
在这里插入图片描述
DOM 标准规定事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。

事件捕获阶段:实际目标 div 在捕获阶段不会接收事件。也就是说在捕获阶段,事件从 document 到 html 再到 body 就停止了。上图中为 1 - 3。

处于目标阶段:事件在 div 上发生并处理。但是事件处理会被看成是冒泡阶段的一部分。

冒泡阶段:事件又传播回文档。

事件监听器

事件监听器,又被称之为事件处理程序。简单来说,就是和事件绑定在一起的函数。

当事件发生时就会执行函数中相应的代码。事件监听器根据 DOM 级别的不同写法上也是有所区别的,不仅写法有区别,功能上也是逐渐的在完善。

HTML 事件监听器

HTML 事件监听器,又被称之为行内事件监听器。这是在浏览器中处理事件最原始的方法。我们在最开始的时候写的事件快速入门案例就是一个 HTML 事件处理程序。

具体的示例如下:

<body>
    <button onclick="test()">点击我</button>
    <script>
        let test = function(){
            alert("我知道你已经点击了");
        }
    </script>
</body>

但是有一点需要注意,就是这种方法已经过时了,原因如下:

JavaScript 代码与 HTML 标记混杂在一起,破坏了结构和行为分离的理念。
每个元素只能为每种事件类型绑定一个事件处理器。
事件处理器的代码隐藏于标记中,很难找到事件是在哪里声明的。
但是如果是做简单的事件测试,那么这种写法还是非常方便快捷的。

DOM 0 级事件监听器

这种方式是首先取到要为其绑定事件的元素节点对象,然后给这些节点对象的事件处理属性赋值一个函数。这样就可以达到 JavaScript 代码和 HTML 代码相分离的目的。

具体的示例如下:

<body>
    <button id="test">点击我</button>
    <script>
        let test = document.getElementById("test");
        test.onclick = function(){
            console.log("this is a test");
        }
    </script>
</body>

这种方式虽然相比 HTML 事件监听器有所改进,但是它也有一个缺点,那就是它依然存在每个元素只能绑定一个函数的局限性。

下面我们尝试使用这种方式为同一个元素节点绑定 2 个事件,如下:

<body>
    <button id="test">点击我</button>
    <script>
        let test = document.getElementById("test");
        test.onclick = function(){
            console.log("this is a test");
        }
        test.onclick = function(){
            console.log("this is a test,too");
        }
    </script>
</body>

效果:点击按钮会只会弹出后面的"this is a test,too",因为后面的事件处理程序把前面的事件处理程序给覆盖掉了。

DOM 2 级事件监听器

DOM 2 级事件处理程序通过addEventListener()可以为一个元素添加多个事件处理程序。

这个方法接收 3 个参数:事件名,事件处理函数,布尔值。如果这个布尔值为 true,则在捕获阶段处理事件,如果为 false,则在冒泡阶段处理事件。若最后的布尔值不填写,则和 false 效果一样,也就是说默认为 false,在冒泡阶段进行事件的处理。

接下来我们来看下面的示例:这里我们为 button 元素绑定了 2 个事件处理程序,并且 2 个事件处理程序都是通过点击来触发。

<body>
    <button id="test">点击我</button>
    <script>
        let test = document.getElementById("test");
        test.addEventListener("click",function(){
            console.log("this is a test");
        },false);
        test.addEventListener("click",function(){
            console.log("this is a test,too");
        },false);
    </script>
</body>

效果:可以看到绑定在上面的 2 个事件处理程序都工作正常。

注:在 Internet Explorer 浏览器中使用的方法为attachEvent()。

删除事件监听器

通过 DOM 0 级来添加的事件,删除的方法很简单,只需要将节点对象的事件处理属性赋值为 null 即可。

具体的示例如下:

<body>
    <button id="test">点击我</button>
    <script>
        let test = document.getElementById("test");
        test.onclick = function(){
            console.log("this is a test");
        }
        test.onclick = null; // 删除事件绑定
    </script>
</body>

通过 DOM 2 级来添加的事件,我们可以使用removeEventLister()来进行事件的删除。

需要注意的是,如果要通过该方法移除某一类事件类型的一个事件的话,在通过addEventListener()来绑定事件时的写法就要稍作改变。

先单独将绑定函数写好,然后addEventListener()进行绑定时第 2 个参数传入要绑定的函数名即可。

具体的示例如下:

<body>
    <button id="test">点击我</button>
    <script>
        let test = document.getElementById("test");
        //DOM 2级添加事件
        let fn1 = function(){
            console.log("this is a test");
        }
        let fn2 = function(){
            console.log("this is a test,too");
        }
        test.addEventListener("click",fn1,false);
        test.addEventListener("click",fn2,false);
        test.removeEventListener("click",fn1); // 只删除第一个点击事件
    </script>
</body>

效果:第一个点击事件已经被移除掉了

注:在 Internet Explorer 浏览器中使用的方法为detachEvent()。

事件类型

鼠标事件

鼠标事件是 Web 开发中最常见的一类事件。DOM 3 级标准中定义了 9 个鼠标事件。

click:用户单机主鼠标按钮(一般是左键)或者按下回车键时触发。
dblclick:用户双击鼠标按钮时触发。
mousedown:用户按下了任意鼠标按钮时触发。
mouseenter:进入元素时触发,但是再进入到子元素时不会再触发。
mouseleave:从某个元素出来时触发,但不包括子元素。
mousemove:当鼠标在元素范围内移动内,就会不停的重复地触发。
mouseout:从当前元素出来时触发,进出子元素也会触发。(因为进出子元素可以看作是从当前元素出来了)
mouseover:进入元素时触发,进出子元素时也会触发。(因为进出子元素可以看作是进入了新的元素)
mouseup:在用户释放鼠标按钮时触发。

键盘事件

常见的键盘事件有 3 个,分别为keydown,keypress和keyup,介绍如下:

keydown:按下键盘上任意键时触发,而且如果按住不放的话,会重复触发此事件。
keypress:按下键盘上一个字符键(不包括 shift 和 alt 键)时触发。如果按住不放,也是会重复触发。
keyup:当用户释放键盘上的键时触发。
注:所谓字符键,是指按下后会产生一个字符值的按键,例如 A、B、C…等。而 shift 或者 alt 等功能按键则不是字符键。

这里我们演示一个 keydown 事件,如下:

<body>
    <script>
        document.onkeydown = function(){
            console.log('你按下了键盘上的某一个键');
        }
    </script>
</body>

效果:当我们随意按下键盘上的任意按键时,会触发所绑定的事件。

页面事件

页面事件主要是指当我们对整个 HTML 页面做相应的操作时会触发的事件。常见的有页面加载,页面卸载以及页面滚动等。

页面加载

页面事件中最常用的一个事件就是load事件。这个事件一般绑定在 window 对象上面。页面完全加载后(包括所有图像,JavaScript 文件,CSS 外部资源),就会触发 window 上面的load事件。

最常见的写法如下:

<body>
    <p>在看到我之前,应该有一个弹出框</p>
    <script>
        window.onload = function(){
            alert("你正在加载一个页面!");
        }
    </script>
</body>

效果:首先页面完全加载后,触发load事件,出现弹出框。然后页面才被加载出来。

当然该事件也可以用于某个单独的元素上面,示例如下:

<body>
    <p>在看到我之前,应该有一个弹出框</p>
    <img src="./1.jpg" alt="" onload="alert('Image loaded')">
</body>

效果:上面的代码表示,当图像加载完毕以后就会显示一个警告框。

页面卸载

与load事件对应的是unload事件,这个事件是在文档完全被卸载后触发。只要用户从一个页面切换到另一个页面,或者关闭浏览器,就会触发这个事件。示例如下:

注:现代浏览器规定当页面被卸载时,alert(),confim(),prompt()等模态对话框不再允许弹出,所以这里无法显示出其效果。

<body>
    <p>Lorem ipsum dolor sit amet.</p>
    <script>
        window.onunload = function(){
            console.log('页面已经卸载');
        }
    </script>
</body>

效果:由于刷新也算是一种页面卸载,所以不停的刷新页面可以勉强在控制台中看到打印的语句。

页面滚动

滚动页面时,对应的scroll滚动事件就会被触发。通过 body 元素的scrollLeft和scrollTop可以监控到这一变化。scrollLeft 会监控横向滚动条的变化,scrollTop 会监控垂直方向的滚动条变化。

具体的示例如下:

<body style="height:5000px;" id="body">
    <p>Lorem ipsum dolor sit amet.</p>
    <script>
        document.body.onscroll = function(){
            console.log(document.documentElement.scrollTop);
        }
    </script>
</body>

效果:滚动页面时在控制台会打印出当前滚动了的高度。

窗口设置大小

当浏览器窗口被调整到一个新的高度或者宽度时,就会触发resize事件。

具体的示例如下:

<body>
    <p>Lorem ipsum dolor sit amet.</p>
    <script>
        window.onresize = function(){
            console.log("窗口大小被重置了");
        }
    </script>
</body>

我们可以通过document.body.clientHeight以及document.body.clientWidth来获取 body 元素的宽高,配合resize事件,可以看到这两个属性实时变化的一个效果。

<body>
    <p>Lorem ipsum dolor sit amet.</p>
    <script>
        window.onresize = function(){
            console.clear(); // 清空控制台信息
            console.log(document.body.clientHeight); // 高度会根据当前的页面高度而固定
            console.log(document.body.clientWidth);
        }
    </script>
</body>

事件对象

每当这些事件被触发时,事件监听器就会执行相应的代码。

与此同时,监听器上所绑定的回调函数里面还会自动传入一个叫做 event 的事件对象。该对象包含了许多和当前所触发的事件相关的信息。例如绑定事件的 DOM 元素是什么,所触发的事件类型是什么等。

这一小节我们就一起来看一下有关事件对象的相关知识。

首先,事件对象是以事件处理函数中的参数形式出现的,该对象并不需要我们自己创建,直接使用即可。

语法结构如下:

事件源.addEventListener(eventName, function(event){
// event 就是事件对象
}, boolean)

事件对象说明:

  • 当事件发生时,只能在事件函数内部访问的对象
  • 处理函数结束后会自动销毁

兼容的事件对象

这里有必要说一下获取事件对象的方式。上面有提到,事件对象是以事件处理函数中的参数形式出现的。在 DOM 标准以及 Internet Explorer 9 及之后版本的浏览器中,确实是这样的。

具体的示例如下:

btn.onclick = function(event){
    console.log(event);
}

而在 Internet Explorer 8 及之前的版本浏览器中,event 事件对象是作为 window 对象的一个属性。

具体的示例如下:

btn.onclick = function(event){
    console.log(window.event);
}

想要实现 event 事件对象的兼容,我们可以在事件的处理函数中添加以下代码:

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

通用的事件对象属性

所谓通用的事件对象属性,就是指无论你触发的是什么事件,事件对象中都会拥有的属性。最常见的两个属性就是事件类型以及事件目标。

事件类型

事件对象的type属性会返回当前所触发的事件类型是什么。

具体的示例如下:

<body>
    <button id="test">点击我</button>
    <script>
        let test = document.getElementById("test");
        test.addEventListener("click",function(event){
            console.log(event.type); // click
        },false);
    </script>
</body>

注:event 对象会自动被传入到所绑定的回调函数里面,所以即便回调函数中没有书写以 event 作为的形参,函数内部依然可以正常使用。但是我们建议一般还是写上比较好。

事件目标

target属性返回对触发事件的元素节点对象的一个引用。

具体的示例如下:

<body>
    <button id="test">点击我</button>
    <script>
        let test = document.getElementById("test");
        test.addEventListener("click",function(){
            console.log(event.target);//<button id="test">点击我</button>
        },false);
    </script>
</body>
this 值

这里要重点提一下事件处理函数里面的this值。

当我们触发一个事件的时候,事件处理程序里面的 this 指代的是绑定事件的元素,而事件对象的target属性指代的是触发事件的元素。

注:target和srcElememnt是等价的。

鼠标事件的事件对象

当我们触发鼠标事件时,会有一些与鼠标事件相关的属性被填入到 event 对象里面。

button属性

当我们按下鼠标时,会有一个button属性被填入到 event 对象里面。

button 属性有 3 个值:0 表示按下的是鼠标左键,1 表示按下的是鼠标中键,2 表示按下的是鼠标右键。

具体的示例如下:

<body>
    <p id="test">Lorem ipsum dolor sit amet.</p>
    <script>
        test.onmousedown = function(event){
            console.log(event.button);
        }
    </script>
</body>

效果:按下鼠标不同的键位显示出不同的数字

事件坐标

除了获取用户按下的是哪一个鼠标键以外,我们还能够精准的获取到用户按下鼠标时所处的位置。这里获取的坐标值有好几组,大致如下:

相对于浏览器器位置:

event.clientX:返回当事件被触发时鼠标指针相对于浏览器页面(或客户区)的水平坐标。(只包含文档的可见部分,不包含窗口本身的控件以及滚动条)
event.clientY:返回当事件被触发时⿏标指针相对于浏览器页面(客户区)的垂直坐标。(只包含文档的可见部分,不包含窗口本身的控件以及滚动条)
event.pageX:可以获取事件发生时光标相对于当前窗口的水平坐标信息(包含窗口自身的控件和滚动条)
event.pageY:可以获取事件发生时光标相对于当前窗口的垂直坐标信息(包含窗口自身的控件和滚动条)

相对于屏幕位置:

event.screenX:返回事件发生时鼠标指针相对于电脑屏幕左上角的水平坐标。
event.screenY:返回事件发生时鼠标指针相对于电脑屏幕左上角的垂直坐标。

相对于事件源位置:

event.offsetX:返回事件发生时鼠标指针相对于事件源的水平坐标
event.offsetY:返回事件发生时鼠标指针相对于事件源的垂直坐标
event.layerX:返回事件发生时鼠标指针相对于事件源的水平坐标(Firefox)
event.layerY:返回事件发生时鼠标指针相对于事件源的垂直坐标(Firefox)
这几个事件属性看似类似,但是有一些细微的不同。它们有助于查明点击的位置,或者鼠标光标的位置。

具体的示例如下:

<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        body {
            height: 5000px;
        }
        div {
            position: fixed;
            top: 50px;
            left: 100px;
            width: 100px;
            height: 100px;
            border: 1px solid;
        }
    </style>
</head>

<body>
    <div id="box"></div>
    <script>
        box.addEventListener("click", function () {
            console.log("screen:", event.screenX, event.screenY);
            console.log("page:", event.pageX, event.pageY);
            console.log("client:", event.clientX, event.clientY);
            console.log("offset:", event.offsetX, event.offsetY);
        }, false);
    </script>
</body>

效果:将窗口向下滚动一段距离后,点击鼠标,出现了以下数值。

这里分别做一些解释,如下:

screen:参考点为电脑屏幕左上角。
page:参考点为文档左上角,这里由于我滚动了页面,所以滚动的部分也会被记入其中。
client:参照点始终为当前视口的左上角,无论是否滚动了页面。
offset:当前事件源的左上角作为参考点。

键盘事件的事件对象

同样的,键盘事件也会有相关的属性被传入 event 对象,常见的属性有keyCode和key属性。

keyCode 属性会发生在keydown和keyup事件被触发时,每一个 keyCode 的值与键盘上一个特定的键对应。该属性的值与 ASCII 码中对应的编码相同。

<body>
    <input type="text" id="test">
    <script>    
        let test = document.getElementById("test");
        test.addEventListener("keypress",function(){
            console.log("你已经触发了事件");
            console.log(event.keyCode);
        },false);
    </script>
</body>

效果:按下一个键,就会有对应的keyCode被打印到控制台
而我们在做游戏时,常用的 4 个方向按键,左上右下(顺时针)的键码分别是 37、38、39、40。

key属性是为了取代 keyCode 而新增的,它的值是一个字符串。在按下某个字符键时,key 的值就是相应的文本字符。在按下非字符键时,key 的值是相应键的名,默认为空字符串。

注:Internet Explorer 8 以下的浏览器不支持,safari 浏览器不支持keypress事件中的 key 属性。

<body>
    <input type="text" id="test">
    <script>    
        let test = document.getElementById("test");
        test.addEventListener("keydown",function(){
            console.log("你已经触发了事件");
            console.log(event.key);
        },false);
    </script>
</body>

效果:会打印出用户按下的哪一个键

辅助按键(扩展)

按下键盘上的shift,Ctrl,Alt和Meta(Mac上的command)等辅助按键的时候,会触发keydown和keyup事件,但是不会触发keypress事件,因为它们不会在屏幕上产生任何字符。

这里大家可以通过下面两段代码自行进行对比,如下:

<body>
    <script>
        window.onkeyup = function(){
            console.log("OK");
        }
    </script>
</body>

效果:此时按下shift,Ctrl,Alt和Meta(Mac上的command)等辅助按键时将会触发事件。

<body>
    <script>
        window.onkeypress = function(){
            console.log("OK");
        }
    </script>
</body>

效果:此时按下shift,Ctrl,Alt和Meta(Mac上的command)等辅助按键时不会触发事件。

事件对象还有shiftKey,ctrlKey,altKey和metaKey属性,可以返回在键盘事件发生时,是否按下了对应的辅助按键。如果这几个属性返回 true ,就表示对应的键被按下了。

例如:如下的代码会检测在用户按下c键的同时,是否按下了Ctrl键。

<body>
    <script>
        window.onkeydown = function(){
            if((event.key === 'c') && event.ctrlKey){
                console.log("Yes,you pressed ctrl and c");
            }
        }
    </script>
</body>

使用下面的代码可以检测在鼠标单击时,是否按下了Shift键,如下:

<body>
    <script>
        window.onclick = function(){
            if(event.shiftKey){
                console.log("Yes,you hold shiftKey and click the mouse");
            }
        }
    </script>
</body>

阻止冒泡

既然介绍到了事件对象,那么有必要说一说被用的很多的stopPropagation()方法。

首先,该方法是事件对象上面的一个方法。其次,该方法的主要作用就是用来阻止冒泡的。那什么又是阻止冒泡?为什么要阻止冒泡?下面我们来对此进行一个具体的介绍。

在前面介绍事件流的时候,我们有介绍过事件冒泡。但是事件冒泡带给我们很不方便的一点在于,如果父级元素和后代元素都绑定了相同的事件,那么后代元素触发事件时,父级元素的事件也会被触发。

这里我们来看一个示例如下:

<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        .father{
            width: 200px;
            height: 200px;
            background-color: pink;
        }
        .son{
            width: 100px;
            height: 100px;
            background-color: skyblue;
            position: absolute;
            left: 500px;
        }
    </style>
</head>
<body>
    <div class="father" onclick="test()">
        <div class="son" onclick="test2()"></div>
    </div>
    <script>
        let test = function(){
            console.log("你点击了父元素");
        }
        let test2 = function(){
            console.log("你点击了子元素");
        }
    </script>
</body>

效果:当我们点击了子元素之后,父元素上面绑定的事件也同时被触发
通常情况下我们都是一步到位,明确自己的事件触发源,并不希望浏览器自作聪明、漫无目的地去帮我们找合适的事件处理程序,所以这种情况下我们不需要事件冒泡。

另外,通过对事件冒泡的理解,我们知道事件冒泡会让程序将做很多额外的事情,这必然增大程序开销。事件冒泡处理可能会激活我们本来不想激活的事件,导致程序错乱,甚至无从下手调试,这常成为对事件冒泡不熟悉程序员的棘手问题。

所以必要时,我们要阻止事件冒泡。

我们可以使用事件对象的stopPropagation()方法来阻止冒泡。下面的例子演示了在现代浏览器中通过stopPropagation()来阻止冒泡的方式。

<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        .father{
            width: 200px;
            height: 200px;
            background-color: pink;
        }
        .son{
            width: 100px;
            height: 100px;
            background-color: skyblue;
            position: absolute;
            left: 500px;
        }
    </style>
</head>
<body>
    <div class="father" onclick="test()">
        <div class="son" onclick="test2()"></div>
    </div>
    <script>
        let test = function(){
            console.log("你点击了父元素");
        }
        let test2 = function(event){
            console.log("你点击了子元素");
            event.stopPropagation(); // 阻止事件向上冒泡
        }
    </script>
</body>   

效果:当我们再次点击子元素来触发事件时,不会再同时触发绑定在父元素上面的事件。

阻止默认行为

除了上面所介绍的阻止冒泡,我们还可以阻止一些浏览器的默认行为。常见的默认行为有点击链接后,浏览器跳转到指定页面,或者按一下空格键,页面向下滚动一段距离等。

举个例子:点击下面的超链接时,浏览器默认行为会跳转到百度。

<a href="http://www.baidu.com">百度</a>

但是有些时候,我们希望阻止这一默认行为。

关于取消默认行为的方式有cancelable、defaultPrevented、preventDefault()和returnValue。不同的 DOM 级别,取消默认行为的方式稍微差异。

在 DOM 0 级事件处理程序中,使用returnValue、preventDefault()和return false都有效。

在 DOM 2 级事件处理程序中,使用return false无效。

在 Internet Explorer 事件处理程序中,使用preventDefault()无效。

cancelable

cancelable属性返回一个布尔值,表示事件是否可以取消。该属性为只读属性。返回 true 时,表示可以取消。否则,表示不可取消。

注:Internet Explorer 8 及以下浏览器不支持。

<body>
    <a id="test" href="http://www.baidu.com">百度</a>
    <script>
         let test = document.getElementById("test");
         test.onclick = function(event){
            test.innerHTML = event.cancelable; // true
         }
    </script>
</body>

效果:首先 a 标签的文本内容会变为 true,然后跳转到百度页面。表示该跳转的默认行为是能够取消的。

preventDefault() 方法

preventDefault()方法是 DOM 中最标准的取消浏览器默认行为的方式,无返回值。

注:Internet Explorer 8 及以下浏览器不支持。

<body>
    <a id="test" href="http://www.baidu.com">百度</a>
    <script>
         let test = document.getElementById("test");
         test.onclick = function(event){
             event.preventDefault();
         }
    </script>
</body>

returnValue 属性

该属性可读写,默认值是 true,将其设置为 false 就可以取消事件的默认行为,与preventDefault()方法的作用相同。

最早在 Internet Explorer 的事件对象中实现了这种取消默认行为的方式,但是现在大多数浏览器都实现了该方式。

注:firefox 和 Internet Explorer 9 以上浏览器不支持。

<body>
    <a id="test" href="http://www.baidu.com">百度</a>
    <script>
         let test = document.getElementById("test");
         test.onclick = function(event){
            event.returnValue = false;
         }
    </script>
</body>

return false

除了以上方法外,取消默认事件还可以使用return false

<body>
    <a id="test" href="http://www.baidu.com">百度</a>
    <script>
         let test = document.getElementById("test");
         test.onclick = function(){
            return false;
         }
    </script>
</body>

defaultPrevented() 方法(扩展)

defaultPrevented属性表示默认行为是否被阻止,返回 true 时表示被阻止,返回 false 时,表示未被阻止。

注:Internet Explorer 8 及以下浏览器不支持。

<body>
    <a id="test" href="http://www.baidu.com">百度</a>
    <script>
        let test = document.getElementById("test");
        test.onclick = function(event){
            // 采用两种不同的方式来阻止浏览器默认行为,这是为了照顾其兼容性
            if(event.preventDefault){
                event.preventDefault();
            }else{
                event.returnValue = false;
            }
            // 将是否阻止默认行为的结果赋值给 <a> 标签的文本内容
            test.innerHTML = event.defaultPrevented;
        }
    </script>
</body>

效果:点击<a> 标签之后里面的文本内容会变为true,因为默认行为已经被阻止。

事件流(扩展)

事件对象的eventPhase属性可以返回一个整数值,表示事件目前所处的事件流阶段。0 表示事件没有发生,1 表示当前事件流处于捕获阶段,2 表示处于目标阶段,3 表示冒泡阶段。

注:Internet Explorer 8 及以下浏览器不支持。

<body>
    <button id="test">点击我</button>
    <script>
        test.onclick = function(){
            test.innerText = event.eventPhase; // 2
        }
    </script>
</body>

效果:点击按钮后里面的文本变为2,说明处于目标阶段。

<body>
    <button id="test">点击我</button>
    <script>
        document.addEventListener("click",function(){
            test.innerText = event.eventPhase; // 1
        },true); // 最后的布尔参数值为 true,说明在捕获阶段处理事件
    </script>
</body>

效果:点击按钮后里面的文本变为 1,说明处于捕获阶段。

<body>
    <button id="test">点击我</button>
    <script>
        document.addEventListener("click",function(){
            test.innerText = event.eventPhase; // 3
        },false); // 最后的布尔参数值为 false,说明在冒泡阶段处理事件
    </script>
</body>

效果:点击按钮后里面的文本变为3,说明处于冒泡阶段。

事件委托

前面在介绍事件冒泡的时候,讲过了事件冒泡的缺点,所以必要的时候,我们需要阻止事件冒泡。但是事件冒泡并不是说只有缺点没有优点,事件冒泡一个最大的好处就是可以实现事件委托。

事件委托,又被称之为事件代理。在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面整体的运行性能。导致这一问题的原因是多方面的。

首先,每个函数都是对象,都会占用内存。内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。

对事件处理程序过多问题的解决方案就是事件委托。

事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click 事件会一直冒泡到 document 层次。也就是说,我们可以为整个页面指定一个 onclick 事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。

举一个具体的例子:例如现在我的列表项有如下内容

<body>
    <ul id="color-list">
        <li>red</li>
        <li>yellow</li>
        <li>blue</li>
        <li>green</li>
        <li>black</li>
        <li>white</li>
    </ul>
</body>

如果我们想把事件监听器绑定到所有的 li 元素上面,这样它们被单击的时候就弹出一些文字,为此我们需要给每一个元素来绑定一个事件监听器。

虽然上面的例子中好像问题也不大,但是想象一下如果这个列表有 100 个元素,那我们就需要添加 100 个事件监听器,这个工作量还是很恐怖的。

这个时候我们就可以利用事件代理来帮助我们解决这个问题。将事件监听器绑定到父元素 ul 上,这样即可对所有的 li 元素添加事件,如下:

<body>
    <ul id="color-list">
        <li>red</li>
        <li>yellow</li>
        <li>blue</li>
        <li>green</li>
        <li>black</li>
        <li>white</li>
    </ul>
    <script>
        let colorList = document.getElementById("color-list");
        colorList.addEventListener("click",function(){
            alert("Hello");
        })
    </script>
</body>

现在我们单击列表中的任何一个 li 都会弹出东西,就好像这些 li 元素就是 click 事件的目标一样。并且如果我们之后再为这个 ul 添加新的 li 元素的话,新的 li 元素也会自动添加上相同的事件。

但是,这个时候也存在一个问题,虽然我们使用事件代理避免了为每一个 li 元素添加相同的事件,但是如果用户没有点击 li,而是点击的 ul,同样也会触发事件。这也很正常,因为我们事件就是绑定在 ul 上面的。

此时我们可以对点击的节点进行一个小小的判断,从而保证用户只在点击 li 的时候才触发事件,如下:

<body>
    <ul id="color-list">
        <li>red</li>
        <li>yellow</li>
        <li>blue</li>
        <li>green</li>
        <li>black</li>
        <li>white</li>
    </ul>
    <script>
        let colorList = document.getElementById("color-list");
        colorList.addEventListener("click", function (event) {
            if (event.target.nodeName === 'LI') {
                alert('点击 li');
            }
        })
    </script>
</body>
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值