事件

JS 事件(event)是当用户与网页进行交互时发生的事情,例如单机某个链接或按钮、在文本框中输入文本、按下键盘上的某个按键、移动鼠标等等。当事件发生时,您可以使用 JavaScript 中的事件处理程序(也可称为事件监听器)来检测并执行某些特定的程序。
一般情况下事件的名称都是以单词 on 开头的,例如点击事件 onclick、页面加载事件 onload 等。下表中列举了一些 JavaScript 中常用的事件:
image

事件绑定

事件只有与 HTML 元素绑定之后才能被触发,为 HTML 元素绑定事件处理程序的方法由很多,最简单的就是通过 HTML 事件属性来直接绑定事件处理程序,例如 onclick 、onmouseover 、onmouseout 等属性。
以 onclick 属性为例,通过该属性我们可以为指定的 HTML 元素定义鼠标点击事件(即在该元素上单击鼠标左键时触发的事件),示例代码如下:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>JavaScript</title>
    </head>
    <body>
        <button type="button" onclick="myBtn()">按钮</button>
        <script type="text/javascript">
            function myBtn(){
                alert("Hello World!");
            }
        </script>
    </body>
    </html>

除了上述方法外,我们也可以直接使用 JavaScript 中提供的内置函数来为指定元素绑定事件处理程序,如下例所示:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>JavaScript</title>
    </head>
    <body>
        <button type="button" id="myBtn">按钮</button>
        <script>
            function sayHello() {
                alert('Hello World!');
            }
            document.getElementById("myBtn").onclick = sayHello;
        </script>
    </body>
    </html>

事件示例

一般情况下,事件可以分为四大类——鼠标事件、键盘事件、表单事件和窗口事件,另外还有一些其它事件。下面通过几个示例来简单介绍一些比较常用的事件。

  1. onmouseover 事件
    onmouseover 事件就是指当用户鼠标指针移动到元素上时触发的事件,示例代码如下
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>JavaScript</title>
    </head>
    <body>
        <button type="button" onmouseover="alert('您的鼠标已经移动到了该按钮上');">请将鼠标移动至此处</button><br>
        <a href="#" onmouseover="myEvent()">请将鼠标移动至此处</a>
        <script>
            function myEvent() {
                alert('您的鼠标已经移动到了该链接上');
            }
        </script>
    </body>
    </html>
  1. onmouseout 事件
    onmouseout 事件与 onmouseover 事件正好相反,onmouseout 事件会在鼠标从元素上离开时触发,示例代码如下
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>JavaScript</title>
    </head>
    <body>
        <div style="width: 350px; height: 200px; border:1px solid black" id="myBox"></div>
        <script>
            function myEvent() {
                alert('您的鼠标已经离开指定元素');
            }
            document.getElementById("myBox").onmouseout = myEvent;
        </script>
    </body>
    </html>
  1. onkeydown 事件
    onkeydown 事件是指当用户按下键盘上的某个按键时触发的事件,示例代码如下
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>JavaScript</title>
    </head>
    <body>
        <input type="text" onkeydown="myEvent()">
        <script>
            function myEvent() {
                alert("您按下了键盘上的某个按钮");
            }
        </script>
    </body>
    </html>
  1. onkeyup 事件
    onkeyup 事件是指当用户按下键盘上的某个按键并将其释放(即按下并松开某个按键)时触发的事件,示例代码如下:
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>JavaScript</title>
    </head>
    <body>
        <input type="text" onkeyup="myEvent()">
        <script>
            function myEvent() {
                alert("您按下了键盘上的某个按钮,并将其释放了");
            }
        </script>
    </body>
    </html>

事件冒泡与事件捕获

在 JavaScript 中,我们将事件发生的顺序称为“事件流”,当我们触发某个事件时,会发生一些列的连锁反应,例如有如下所示的一段代码:

    <body>
        <div id="wrap">
            <p class="hint">
                <a href="#">Click Me</a>
            </p>
        </div>
    </body>

如果给每个标签都定义事件,当我们点击其中的 <a> 标签时,会发现绑定在 <div> 和 <p> 标签上的事件也被触发了,这到底是为什么呢?为了解答这一问题,微软和网景两公司提出了两种不同的概念,事件捕获与事件冒泡:

  • 事件捕获:由微软公司提出,事件从文档根节点(Document 对象)流向目标节点,途中会经过目标节点的各个父级节点,并在这些节点上触发捕获事件,直至到达事件的目标节点
  • 事件冒泡:由网景公司提出,与事件捕获相反,事件会从目标节点流向文档根节点,途中会经过目标节点的各个父级节点,并在这些节点上触发捕获事件,直至到达文档的根节点。整个过程就像水中的气泡一样,从水底向上运动

提示:上面提到的目标节点指的是触发事件的节点。

后来,W3C 为了统一标准,采用了一个折中的方式,即将事件捕获与事件冒泡合并,也就是现在的“先捕获后冒泡”,如下图所示:
image

事件捕获

在事件捕获阶段,事件会从 DOM 树的最外层开始,依次经过目标节点的各个父节点,并触发父节点上的事件,直至到达事件的目标节点。以上图中的代码为例,如果单击其中的 <a> 标签,则该事件将通过 document -> div -> p -> a 的顺序传递到 <a> 标签。示例代码如下

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>JavaScript</title>
        <style type="text/css">
            div, p, a {
                padding: 15px 30px;
                display: block;
                border: 2px solid #000;
                background: #fff;
            }
        </style>
    </head>
    <body>
        <div id="wrap">DIV
            <p class="hint">P
                <a href="#">A</a>
            </p>
        </div>
        <script>
            function showTagName() {
                alert("事件捕获: " + this.tagName);
            }
            var elems = document.querySelectorAll("div, p, a");
            for (let elem of elems) {
                elem.addEventListener("click", showTagName, true);
            }
        </script>
    </body>
    </html>

运行上面的代码,单击最内层的 <a> 标签,运行结果如下图所示:
image

事件冒泡

事件冒泡正好与事件捕获相反,事件冒泡是从目标节点开始,沿父节点依次向上,并触发父节点上的事件,直至文档根节点,就像水底的气泡一样,会一直向上。示例代码如下

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>JavaScript</title>
        <style type="text/css">
            div, p, a {
                padding: 15px 30px;
                display: block;
                border: 2px solid #000;
                background: #fff;
            }
        </style>
    </head>
    <body>
        <div onclick="alert('事件冒泡: ' + this.tagName)">DIV
            <p onclick="alert('事件冒泡: ' + this.tagName)">P
                <a href="#" onclick="alert('事件冒泡: ' + this.tagName)">A</a>
            </p>
        </div>
    </body>
    </html>

运行上面的代码,单击最内层的 <a> 标签,运行结果如下图所示:
image

阻止事件捕获和冒泡

了解了事件捕获和事件冒泡后会发现,这个特性并不友好,例如我们在某个节点上绑定了事件,本想在点击时触发这个事件,结果由于事件冒泡,这个节点的事件被它的子元素给触发了。我们要如何阻止这样的事情发生呢?
JavaScript 中提供了 stopPropagation() 方法来阻止事件捕获和事件冒泡的发生,语法格式如下

event.stopPropagation();

注意:stopPropagation() 会阻止事件捕获和事件冒泡,但是无法阻止标签的默认行为,例如点击链接任然可以打开对应网页。

示例代码如下

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>JavaScript</title>
        <style type="text/css">
            div, p, a {
                padding: 15px 30px;
                display: block;
                border: 2px solid #000;
                background: #fff;
            }
        </style>
    </head>
    <body>
        <div id="wrap">DIV
            <p class="hint">P
                <a href="#">A</a>
            </p>
        </div>
        <script>
            function showAlert(event) {
                alert("您点击了 "+ this.tagName + " 标签");
                event.stopPropagation();
            }
            var elems = document.querySelectorAll("div, p, a");
            for(let elem of elems) {
                elem.addEventListener("click", showAlert);
            }
        </script>
    </body>
    </html>

此外,您也可以使用 stopImmediatePropagation() 方法来阻止同一节点的同一事件的其它事件处理程序,例如为某个节点定义了多个点击事件,当事件触发时,这些事件会按定义顺序依次执行,如果其中一个事件处理程序中使用了 stopImmediatePropagation() 方法,那么剩下的事件处理程序将不再执行。stopImmediatePropagation() 方法的语法格式如下

event.stopImmediatePropagation();

示例代码如下

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>JavaScript</title>
        <style type="text/css">
            div, p, a {
                padding: 15px 30px;
                display: block;
                border: 2px solid #000;
                background: #fff;
            }
        </style>
    </head>
    <body>
        <div onclick="alert('您点击了 ' + this.tagName + ' 标签')">DIV
            <p onclick="alert('您点击了 ' + this.tagName + ' 标签')">P
                <a href="#" id="link">A</a>
            </p>
        </div>
        <script>
            function sayHi() {
                alert("事件处理程序 1");
                event.stopImmediatePropagation();
            }
            function sayHello() {
                alert("事件处理程序 2");
            }
            // 为 id 为 link 的标签定义多个点击事件
            var link = document.getElementById("link");
            link.addEventListener("click", sayHi);
            link.addEventListener("click", sayHello);
        </script>
    </body>
    </html>

阻止默认操作

某些事件具有与之关联的默认操作,例如当您单击某个链接时,会自动跳转到指定的页面,当您单击提交按钮时,会将数据提交到服务器等。如果不想这样的默认操作发生,可以使用 preventDefault() 方法来阻止,其语法格式如下

event.preventDefault();

示例代码如下

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>JavaScript</title>
    </head>
    <body>
        <a href="http://c.biancheng.net/" id="link">链接</a>
        <script>
            var link = document.getElementById("link");
            link.addEventListener('click', function(event){
                event.preventDefault(); // 阻止链接跳转
            });
        </script>
    </body>
    </html>

注意:IE9 及以下的版本不支持 preventDefault() 方法,对于 IE9 及以下的浏览器您可以使用 event.returnValue = false; 。

事件委托(事件代理)

利用 JS 事件冒泡动态为元素绑定事件的方法称为事件委托(Event Delegation,也称为“事件代理”),是 JavaScript 中最热门的技术之一。
事件委托就是把原本需要绑定在子元素上的事件(onclick 、onkeydown 等)委托给它的父元素,让父元素来监听子元素的冒泡事件,并在子元素发生事件冒泡时找到这个子元素。
举个简单的例子,整个宿舍的同学都需要去取快递,一种方法是让他们一个个去取,另一种方法是把这件事委托给宿舍长,让宿舍长把所有人的快递都取回来,然后再根据收件人一一分发给宿舍的同学。在这里,我们可以将取快递看作一个事件;每个同学看作是需要绑定事件的 DOM 元素;宿舍长看作是这些 DOM 元素的父元素,事件需要绑定在这个父元素上;按照收件人分发快递的过程就是事件执行的过程。

为什么要使用事件委托

在 JavaScript 中,页面内事件处理程序的个数会直接影响页面的整体性能,因为每个事件处理程序都是对象,对象会占用内存,内存中的对象越多,页面的性能则越差。此外,事件处理程序需要与 DOM 节点进行交互,访问 DOM 的次数越多,引起浏览器重绘和重排的次数也就越多,从而影响页面的性能。

重绘是指当元素样式改变时,浏览器会根据元素的新样式重新绘制元素的外观。重排是指当 DOM 树的一部分发生变化时(例如元素尺寸改变),浏览器会重新创建 DOM 树。

当页面中很多表格或列表需要添加事件时,如果逐个添加那就太麻烦了,但是使用事件委托就能极大的减轻我们的工作量,同时也能提高页面的性能。

事件委托实现原理

事件委托是利用事件的冒泡原理来实现的,大致可以分为三个步骤:

  • 确定要添加事件元素的父级元素
  • 给父元素定义事件,监听子元素的冒泡事件
  • 使用 event.target 来定位触发事件冒泡的子元素

注意:使用事件委托时,并不是说把事件委托给随意一个父元素就行。因为事件冒泡的过程也需要消耗时间,距离越远,所需的时间也就越长,所有最好在直接父元素上使用事件委托。

假如我们要为 ul 列表下的每个 li 标签添加点击事件,如果不使用事件委托,最简单的办法就是使用循环来为每个 li 标签绑定事件,示例代码如下:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>JavaScript</title>
    </head>
    <body>
        <ul id="list">
            <li>1</li>
            <li>2</li>
            <li>3</li>
            <li>4</li>
        </ul>
        <script>
            window.onload = function(){
                var the_ul = document.getElementById('list');
                var the_li = the_ul.getElementsByTagName('li');
                for( var i=0; i < the_li.length; i++ ){
                    the_li[i].onclick = function(){
                        console.log(this.innerHTML)
                    }
                }
            }
        </script>
    </body>
    </html>

通过上面的代码可以看出,要为每个 li 标签绑定点击事件,首先需要找到 ul 标签,然后通过 ul 标签找到所有 li 标签, 最后在通过遍历所有 li 标签来绑定事件。若使用事件委托的话,就会简单很多,示例代码如下:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>JavaScript</title>
    </head>
    <body>
        <ul id="list">
            <li>1</li>
            <li>2</li>
            <li>3</li>
            <li>4</li>
        </ul>
        <script>
            window.onload = function(){
                var the_ul = document.getElementById('list');
                the_ul.onclick = function(e){
                    console.log(e.target.innerHTML)
                }
            }
        </script>
    </body>
    </html>

通过代码可以看出,使用事件委托我们只需要为 ul 标签绑定事件,当 li 标签被点击时,由于事件冒泡的特性,会触发 ul 标签上的事件,我们只需要在事件中通过 event 对象中的 target 属性来找到被点击的 li 标签即可。不过这样做也有一个弊端,那就是当我们点击 ul 标签时,也会触发事件。
另外,如果我们需要动态的向 ul 标签中添加 li 标签,同时也需要在新添加的 li 标签中添加点击事件,就必须通过事件委托来实现了,示例代码如下:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>JavaScript</title>
    </head>
    <body>
        <ul id="list" style="width: 100px;margin:0;float: left;">
            <li>1</li>
            <li>2</li>
            <li>3</li>
            <li>4</li>
        </ul>
        <button style="float:left;" id="addli">添加一个 li</button>
        <button style="float:left;" id="delli">删除一个 li</button>
        <script>
            window.onload = function(){
                var the_ul = document.getElementById('list');
                var the_li = the_ul.getElementsByTagName('li');
                var sum = the_li.length
                the_ul.onclick = function(e){
                    console.log(e.target.innerHTML)
                };
                document.getElementById('addli').onclick = function (){
                    var newli = document.createElement("li");
                    newli.innerHTML = ++sum;
                    the_ul.appendChild(newli);
                };
                document.getElementById('delli').onclick = function (){
                    the_ul.firstElementChild.remove();
                };
            }
        </script>
    </body>
    </html>

事件委托的优点

  1. 减小内存消耗
    使用事件委托可以大量节省内存,减少事件的定义,通过上面的示例可以看出,要为 ul 标签下的所有 li 标签添加点击事件,如果分别为每个 li 标签绑定事件,不仅写起来比较繁琐,而且对内存的消耗也非常大。而使用事件委托的方式将点击事件绑定到 ul 标签上,就可以实现监听所有 li 标签,简洁、高效。
  2. 动态绑定事件
    在网页中,有时我们需要动态增加或移除页面中的元素,比如上面示例中动态的在 ul 标签中添加 li 标签,如果不使用事件委托,则需要手动为新增的元素绑定事件,同时为删除的元素解绑事件。而使用事件委托就没有这么麻烦了,无论是增加还是减少 ul 标签中的 li 标签,即不需要再为新增的元素绑定事件,也不需要为删除的元素解绑事件。

总结

要使用事件委托,需要保证事件能够发生冒泡,适合使用事件委托的事件有 click 、mousedown 、mouseup 、keydown 、keyup 、keypress 等。需要注意的是,虽然 mouseover 和 mouseout 事件也会发生事件冒泡,但处理起来非常麻烦,所以不推荐在 mouseover 和 mouseout 事件中使用事件委托。
另外,对于不会发生事件冒泡的事件(例如 load 、unload 、abort 、focus 、blur 等),则无法使用事件委托。

posted @ 2023-07-18 14:32  HopeLive  阅读(35)  评论(0)    收藏  举报