JAVASCRIPT事件模型及事件代理

事件简介

事件的三要素

事件的三要素:事件源、事件、事件驱动程序

  • 事件源:引发后续事件的html标签。
  • 事件:js已经定义好了(见下图)。
  • 事件驱动程序:对样式和html的操作。也就是DOM。

也就是说,我们可以在时间对应的属性中写一些js代码,当事件被触发时,这些代码将会执行。

代码书写步骤如下:(重要)

  • (1)获取事件源:document.getElementById(“box”); // 类似于Android里面的findViewById
  • (2)绑定事件: 事件源box.事件onclick = function(){ 事件驱动程序 };
  • (3)书写事件驱动程序:关于DOM的操作。

最简单的代码举例:(点击box1,然后弹框)

<body>
<div id="box1"></div>

<script type="text/javascript">
    // 1、获取事件源
    var div = document.getElementById("box1");
    // 2、绑定事件
    div.onclick = function () {
        // 3、书写事件驱动程序
        alert("我是弹出的内容");
    }
</script>

</body>

常见的事件如下:


下面针对这事件的三要素,进行分别介绍。

1、获取事件源的方式(DOM节点的获取)

获取事件源的常见方式如下:

var div1 = document.getElementById("box1");      //方式一:通过id获取单个标签

var arr1 = document.getElementsByTagName("div");     //方式二:通过 标签名 获得 标签数组,所以有s

var arr2 = document.getElementsByClassName("hehe");  //方式三:通过 类名 获得 标签数组,所以有s

2、绑定事件的方式

方式一:直接绑定匿名函数

<div id="box1" ></div>

<script type="text/javascript">
    var div1 = document.getElementById("box1");
    //绑定事件的第一种方式
    div1.onclick = function () {
        alert("我是弹出的内容");
    }
</script>

方式二:先单独定义函数,再绑定

 <div id="box1" ></div>

<script type="text/javascript">
    var div1 = document.getElementById("box1");
    //绑定事件的第二种方式
    div1.onclick = fn;   //注意,这里是fn,不是fn()。fn()指的是返回值。
    //单独定义函数
    function fn() {
        alert("我是弹出的内容");
    }
</script>

注意上方代码的注释。绑定的时候,是写fn,不是写fn()。fn代表的是整个函数,而fn()代表的是返回值。

方式三:行内绑定

<!--行内绑定-->
<div id="box1" onclick="fn()"></div>

<script type="text/javascript">

    function fn() {
        alert("我是弹出的内容");
    }

</script>

注意第一行代码,绑定时,是写的"fn()",不是写的"fn"。因为绑定的这段代码不是写在js代码里的,而是被识别成了字符串

3、事件驱动程序

我们在上面是拿alert举例,不仅如此,我们还可以操作标签的属性和样式。举例如下:

点击鼠标时,原本粉色的div变大了,背景变红:

    <style>
        #box1 {
            width: 100px;
            height: 100px;
            background-color: pink;
            cursor: pointer;
        }
    </style>
</head>

<body>

<div id="box1" ></div>

<script type="text/javascript">
    var div1 = document.getElementById("box1");
    //点击鼠标时,原本粉色的div变大了,背景变红了
    div1.onclick = function () {
        div1.style.width = "200px";   //属性值要写引号
        div1.style.height = "200px";
        div1.style.backgroundColor = "red";   //属性名是backgroundColor,不是background-color
    }
</script>

上方代码的注意事项:

  • 在js里写属性值时,要用引号
  • 在js里写属性名时,是backgroundColor,不是CSS里面的background-color

绑定事件的两种方式/DOM事件的级别

DOM0的写法:onclick

    element.onclick = function () {

    }

举例:

<body>
<button>点我</button>
<script>
    var btn = document.getElementsByTagName("button")[0];

    //这种事件绑定的方式,如果绑定多个,则后面的会覆盖掉前面的
    btn.onclick = function () {
        console.log("事件1");
    }

    btn.onclick = function () {
        console.log("事件2");
    }

</script>
</body>

点击按钮后,上方代码的打印结果:

事件2

我们可以看到,DOM对象.事件 = 函数的这种绑定事件的方式:一个元素的一个事件只能绑定一个响应函数。如果绑定了多个响应函数,则后者会覆盖前者。

DOM2的写法:addEventListener(高版本浏览器)

    element.addEventListener('click', function () {

    }, false);

参数解释:

  • 参数1:事件名的字符串(注意,没有on)
  • 参数2:回调函数:当事件触发时,该函数会被执行
  • 参数3:true表示捕获阶段触发,false表示冒泡阶段触发(默认)。如果不写,则默认为false。【重要】

举例:

<body>
<button>按钮</button>
<script>
    var btn = document.getElementsByTagName("button")[0];

    // addEventListener: 事件监听器。 原事件被执行的时候,后面绑定的事件照样被执行
    // 这种写法不存在响应函数被覆盖的情况。(更适合团队开发)
    btn.addEventListener("click", fn1);
    btn.addEventListener("click", fn2);

    function fn1() {
        console.log("事件1");
    }

    function fn2() {
        console.log("事件2");
    }

</script>
</body>

点击按钮后,上方代码的打印结果:

    事件1
    事件2

我们可以看到,addEventListener()这种绑定事件的方式:

  • 一个元素的一个事件,可以绑定多个响应函数。不存在响应函数被覆盖的情况。执行顺序是:事件被触发时,响应函数会按照函数的绑定顺序执行。
  • addEventListener()中的this,是绑定事件的对象。
  • addEventListener()不支持 IE8 及以下的浏览器。在IE8中可以使用attachEvent来绑定事件(详见下一小段)。

DOM2的写法:attachEvent(IE8及以下版本浏览器)

    element.attachEvent('onclick', function () {

    });

参数解释:

  • 参数1:事件名的字符串(注意,有on)
  • 参数2:回调函数:当事件触发时,该函数会被执行

举例:

    <body>
        <button>按钮</button>
        <script>
            var btn = document.getElementsByTagName('button')[0];

            btn.attachEvent('onclick', function() {
                console.log('事件1');
            });

            btn.attachEvent('onclick', function() {
                console.log('事件2');
            });
        </script>
    </body>

在低版本的IE浏览器上,点击按钮后,上方代码的打印结果:

    事件2
    事件1

我们可以看到,attachEvent()这种绑定事件的方式:

  • 一个元素的一个事件,可以绑定多个响应函数。不存在响应函数被覆盖的情况。注意:执行顺序是,后绑定的先执行。
  • attachEvent()中的this,是window

兼容性写法

上面的内容里,需要强调的是:

  • addEventListener()中的this,是绑定事件的对象。
  • attachEvent()中的this,是window。

既然这两个写法的this不同,那么,有没有一种兼容性的写法可以确保这两种绑定方式的this是相同的呢?我们可以封装一下。代码如下:

    <body>
        <button>按钮</button>
        <script>
            var btn = document.getElementsByTagName('button')[0];

            myBind(btn , "click" , function(){
                alert(this);
            });



            //定义一个函数,用来为指定元素绑定响应函数
            /*
             * addEventListener()中的this,是绑定事件的对象
             * attachEvent()中的this,是window
             *  需要统一两个方法this
             */
            /*
             * 参数:
             *  element 要绑定事件的对象
             *  eventStr 事件的字符串(不要on)
             *  callback 回调函数
             */
            function myBind(element , eventStr , callback){
                if(element.addEventListener){
                    //大部分浏览器兼容的方式
                    element.addEventListener(eventStr , callback , false);
                }else{
                    /*
                     * this是谁,由调用方式决定
                     * callback.call(element)
                     */
                    //IE8及以下
                    element.attachEvent("on"+eventStr , function(){
                        //在匿名函数 function 中调用回调函数callback
                        callback.call(element);
                    });
                }
            }

        </script>
    </body>

事件对象

当事件的响应函数被触发时,会产生一个事件对象event。浏览器每次都会将这个事件event作为实参传进之前的响应函数。

这个对象中包含了与当前事件相关的一切信息。比如鼠标的坐标、键盘的哪个按键被按下、鼠标滚轮滚动的方向等。

获取 event 对象(兼容性问题)

所有浏览器都支持event对象,但支持的方式不同。如下。

(1)普通浏览器的写法是 event。比如:

(2)ie 678 的写法是 window.event。此时,事件对象 event 是作为window对象的属性保存的。

于是,我们可以采取一种兼容性的写法。如下:

    event = event || window.event; // 兼容性写法

代码举例:

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<script>
    //点击页面的任何部分
    document.onclick = function (event) {
        event = event || window.event; ////兼容性写法

        console.log(event);
        console.log(event.timeStamp);
        console.log(event.bubbles);
        console.log(event.button);
        console.log(event.pageX);
        console.log(event.pageY);
        console.log(event.screenX);
        console.log(event.screenY);
        console.log(event.target);
        console.log(event.type);
        console.log(event.clientX);
        console.log(event.clientY);
    }
</script>
</body>
</html>

event 属性

event 有很多属性,比如:

由于pageX 和 pageY的兼容性不好,我们可以这样做:

  • 鼠标在页面的位置 = 滚动条滚动的距离 + 可视区域的坐标。

使 div 跟随鼠标移动

代码实现:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title></title>
    <style type="text/css">
      #box1 {
        width: 100px;
        height: 100px;
        background-color: red;
        /*
        * 开启box1的绝对定位
        */
        position: absolute;
      }
    </style>

    <script type="text/javascript">
      window.onload = function() {
        /*
         * 使div可以跟随鼠标移动
         */

        //获取box1
        var box1 = document.getElementById("box1");

        //给整个页面绑定:鼠标移动事件
        document.onmousemove = function(event) {
          //兼容的方式获取event对象
          event = event || window.event;

          // 鼠标在页面的位置 = 滚动条滚动的距离 + 可视区域的坐标。
          var pagex = event.pageX || scroll().left + event.clientX;
          var pagey = event.pageY || scroll().top + event.clientY;

          //   设置div的偏移量(相对于整个页面)
          // 注意,如果想通过 style.left 来设置属性,一定要给 box1开启绝对定位。
          box1.style.left = pagex + "px";
          box1.style.top = pagey + "px";
        };
      };

      // scroll 函数封装
      function scroll() {
        return {
          //此函数的返回值是对象
          left: window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop,
          right:
            window.pageXOffset || document.body.scrollLeft || document.documentElement.scrollLeft
        };
      }
    </script>
  </head>
  <body style="height: 1000px;width: 2000px;">
    <div id="box1"></div>
  </body>
</html>

event.target 获取的是触发事件的标签元素

event.currentTarget 获取到的是发起事件的标签元素

DOM事件流

事件传播的三个阶段是:事件捕获、事件冒泡和目标。

  • 事件捕获阶段:事件从祖先元素往子元素查找(DOM树结构),直到捕获到事件目标 target。在这个过程中,默认情况下,事件相应的监听函数是不会被触发的。
  • 事件目标:当到达目标元素之后,执行目标元素该事件相应的处理函数。如果没有绑定监听函数,那就不执行。
  • 事件冒泡阶段:事件从事件目标 target 开始,从子元素往冒泡祖先元素冒泡,直到页面的最上一级标签。

如下图所示:

Snipaste_2020-04-21_12-50-18.png

PS:这个概念类似于 Android 里的 touch 事件传递

事件捕获

addEventListener可以捕获事件:

    box1.addEventListener("click", function () {
        alert("捕获 box3");
    }, true);

上面的方法中,参数为true,代表事件在捕获阶段执行。

重点:捕获阶段,事件依次传递的顺序是:window --> document --> html--> body --> 父元素、子元素、目标元素。

这几个元素在事件捕获阶段的完整写法是:

    window.addEventListener("click", function () {
        alert("捕获 window");
    }, true);

    document.addEventListener("click", function () {
        alert("捕获 document");
    }, true);

    document.documentElement.addEventListener("click", function () {
        alert("捕获 html");
    }, true);

    document.body.addEventListener("click", function () {
        alert("捕获 body");
    }, true);

    fatherBox.addEventListener("click", function () {
        alert("捕获 father");
    }, true);

    childBox.addEventListener("click", function () {
        alert("捕获 child");
    }, true);

说明:

(1)第一个接收到事件的对象是 window(有人会说body,有人会说html,这都是错误的)。

(2)JS中涉及到DOM对象时,有两个对象最常用:window、doucument。它们俩是最先获取到事件的。

补充一个知识点:

在 js中:

  • 如果想获取 html节点,方法是document.documentElement
  • 如果想获取 body 节点,方法是:document.body

二者不要混淆了。

事件冒泡

事件冒泡: 当一个元素上的事件被触发的时候(比如说鼠标点击了一个按钮),同样的事件将会在那个元素的所有祖先元素中被触发。这一过程被称为事件冒泡;这个事件从原始元素开始一直冒泡到DOM树的最上层。

通俗来讲,冒泡指的是:子元素的事件被触发时,父元素的同样的事件也会被触发。取消冒泡就是取消这种机制。

冒泡顺序

一般的浏览器: (除IE6.0之外的浏览器)

  • div -> body -> html -> document -> window

IE6.0:

  • div -> body -> html -> document

不是所有的事件都能冒泡

以下事件不冒泡:blur、focus、load、unload、onmouseenter、onmouseleave。意思是,事件不会往父元素那里传递。

我们检查一个元素是否会冒泡,可以通过事件的以下参数:

    event.bubbles

如果返回值为true,说明该事件会冒泡;反之则相反。

举例:

    box1.onclick = function (event) {
        alert("冒泡 child");

        event = event || window.event;
        console.log(event.bubbles); //打印结果:true。说明 onclick 事件是可以冒泡的
    }

阻止冒泡

大部分情况下,冒泡都是有益的。当然,如果你想阻止冒泡,也是可以的。可以按下面的方法阻止冒泡。

阻止冒泡的方法

w3c的方法:(火狐、谷歌、IE11)

    event.stopPropagation();

IE10以下则是:

event.cancelBubble = true

兼容代码如下:

   box3.onclick = function (event) {

        alert("child");

        //阻止冒泡
        event = event || window.event;

        if (event && event.stopPropagation) {
            event.stopPropagation();
        } else {
            event.cancelBubble = true;
        }
    }

上方代码中,我们对box3进行了阻止冒泡,产生的效果是:事件不会继续传递到 father、grandfather、body了。

事件委托

事件委托,通俗地来讲,就是把一个元素响应事件(click、keydown......)的函数委托到另一个元素。

比如说有一个列表 ul,列表之中有大量的列表项 <a>标签:

<ul id="parent-list">
    <li><a href="javascript:;" class="my_link">超链接一</a></li>
    <li><a href="javascript:;" class="my_link">超链接二</a></li>
    <li><a href="javascript:;" class="my_link">超链接三</a></li>
</ul>

当我们的鼠标移到<a>标签上的时候,需要获取此<a>的相关信息并飘出悬浮窗以显示详细信息,或者当某个<a>被点击的时候需要触发相应的处理事件。我们通常的写法,是为每个<a>都绑定类似onMouseOver或者onClick之类的事件监听:

    window.onload = function(){
        var parentNode = document.getElementById("parent-list");
        var aNodes = parentNode.getElementByTagName("a");
        for(var i=0, l = aNodes.length; i < l; i++){

            aNodes[i].onclick = function() {
                console.log('我是超链接 a 的单击相应函数');
            }
        }
    }

但是,上面的做法过于消耗内存和性能。我们希望,只绑定一次事件,即可应用到多个元素上,即使元素是后来添加的。

因此,比较好的方法就是把这个点击事件绑定到他的父层,也就是 ul 上,然后在执行事件函数的时候再去匹配判断目标元素。如下:

    <!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title></title>
        <script type="text/javascript">
            window.onload = function() {

                // 获取父节点,并为它绑定click单击事件。 false 表示事件在冒泡阶段触发(默认)
                document.getElementById('parent-list').addEventListener('click', function(event) {
                    event = event || window.event;

                    // e.target 表示:触发事件的对象
                    //如果触发事件的对象是我们期望的元素,则执行否则不执行
                    if (event.target && event.target.className == 'link') {
                    // 或者写成 if (event.target && event.target.nodeName.toUpperCase() == 'A') {
                        console.log('我是ul的单击响应函数');
                    }
                }, false);
            };
        </script>
    </head>
    <body>
        <ul id="parent-list" style="background-color: #bfa;">
            <li>
                <p>我是p元素</p>
            </li>
            <li><a href="javascript:;" class="link">超链接一</a></li>
            <li><a href="javascript:;" class="link">超链接二</a></li>
            <li><a href="javascript:;" class="link">超链接三</a></li>
        </ul>
    </body>

上方代码,为父节点注册 click 事件,当子节点被点击的时候,click事件会从子节点开始向父节点冒泡父节点捕获到事件之后,开始执行方法体里的内容:通过判断 event.target 拿到了被点击的子节点<a>。从而可以获取到相应的信息,并作处理。

换而言之,参数为false,说明事件是在冒泡阶段触发(子元素向父元素传递事件)。而父节点注册了事件函数,子节点没有注册事件函数,此时,会在父节点中执行函数体里的代码。

总结:事件委托是利用了冒泡机制,减少了事件绑定的次数,减少内存消耗,提高性能。

鼠标的拖拽事件

拖拽的流程:

(1)onmousedown:当鼠标在被拖拽元素上按下时,开始拖拽;

(2)onmousemove:当鼠标移动时被拖拽元素跟随鼠标移动;

(3)onmouseup:当鼠标松开时,被拖拽元素固定在当前位置。

鼠标的滚轮事件

onmousewheel:鼠标滚轮滚动的事件,会在滚轮滚动时触发。但是火狐不支持该属性。

DOMMouseScroll:在火狐中需要使用 DOMMouseScroll 来绑定滚动事件。注意该事件需要通过addEventListener()函数来绑定。

键盘事件

事件名

onkeydown:按键被按下。

onkeyup:按键被松开。

注意

  • 如果一直按着某一个按键不松手,那么,onkeydown事件会一直触发。此时,松开键盘,onkeyup事件会执行一次。
  • onkeydown连续触发时,第一次和第二次之间会间隔稍微长一点,后续的间隔会非常快。这种设计是为了防止误操作的发生。

键盘事件一般都会绑定给一些可以获取到焦点的对象或者是document。代码举例:

    <body>
        <script>
            document.onkeydown = function(event) {
                event = event || window.event;
                console.log('qianguyihao 键盘按下了');
            };

            document.onkeyup = function() {
                console.log('qianguyihao 键盘松开了');
            };
        </script>

        <input type="text" />
    </body>

判断哪个键盘被按下

可以通过event事件对象的keyCode来获取按键的编码。

此外,event事件对象里面还提供了以下几个属性:

  • altKey
  • ctrlKey
  • shiftKey

上面这三个属性,可以用来判断altctrl、和shift是否被按下。如果按下则返回true,否则返回false。代码举例:

    <body>
        <script>
            document.onkeydown = function(event) {
                event = event || window.event;
                console.log('qianguyihao:键盘按下了');

                // 判断y和ctrl是否同时被按下
                if (event.ctrlKey && event.keyCode === 89) {
                    console.log('ctrl和y都被按下了');
                }
            };
        </script>
    </body>

举例:input 文本框中,禁止输入数字。代码实现:

    <body>
        <input type="text" />

        <script>
            //获取input
            var input = document.getElementsByTagName('input')[0];

            input.onkeydown = function(event) {
                event = event || window.event;

                //console.log('qianguyihao:' + event.keyCode);
                //数字 48 - 57
                //使文本框中不能输入数字
                if (event.keyCode >= 48 && event.keyCode <= 57) {
                    //在文本框中输入内容,属于onkeydown的默认行为
                    return false; // 如果在onkeydown中取消了默认行为,则输入的内容,不会出现在文本框中
                }
            };
        </script>
    </body>

举例:通过键盘的方向键,移动盒子

代码实现:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title></title>
        <style type="text/css">
            #box1 {
                width: 100px;
                height: 100px;
                background-color: red;
                position: absolute;
            }
        </style>
    </head>
    <body>
        <div id="box1"></div>

        <script type="text/javascript">
            // 使div可以根据不同的方向键向不同的方向移动
            /*
             * 按左键,div向左移
             * 按右键,div向右移
             * ...
             */

            //为document绑定一个按键按下的事件
            document.onkeydown = function(event) {
                event = event || window.event;

                //定义一个变量,来表示移动的速度
                var speed = 10;

                //当用户按了ctrl以后,速度加快
                if (event.ctrlKey) {
                    console.log('smyhvae ctrl');
                    speed = 20;
                }

                /*
                 * 37 左
                 * 38 上
                 * 39 右
                 * 40 下
                 */
                switch (event.keyCode) {
                    case 37:
                        //alert("向左"); left值减小
                        box1.style.left = box1.offsetLeft - speed + 'px'; // 在初始值的基础之上,减去 speed 大小
                        break;
                    case 39:
                        //alert("向右");
                        box1.style.left = box1.offsetLeft + speed + 'px';
                        break;
                    case 38:
                        //alert("向上");
                        box1.style.top = box1.offsetTop - speed + 'px';
                        break;
                    case 40:
                        //alert("向下");
                        box1.style.top = box1.offsetTop + speed + 'px';
                        break;
                }
            };
        </script>
    </body>
</html>


上方代码,待改进的地方:

(1)移动盒子时,如果要加速,需要先按方向键,再按Ctrl键

(2)首次移动盒子时,动作较慢。后续如果学习了定时器相关的内容,可以再改进。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 本篇博客源地址 总结: 鼠标事件 1.click与dbclick事件ele.click()ele.click(ha...
    ZombieBrandg阅读 683评论 0 1
  • 总结: 鼠标事件 1.click与dbclick事件$ele.click()$ele.click(handler(...
    阿r阿r阅读 1,639评论 2 10
  • 事件是一种异步编程的实现方式,本质上是程序各个组成部分之间的通信。DOM支持大量的事件,本节介绍DOM的事件编程。...
    许先生__阅读 966评论 0 3
  • (续jQuery基础(1)) 第5章 DOM节点的复制与替换 (1)DOM拷贝clone() 克隆节点是DOM的常...
    凛0_0阅读 1,363评论 0 8
  • 以下文章为转载,对理解JavaScript中的事件处理机制很有帮助,浅显易懂,特分享于此。 什么是事件? 事件(E...
    jxyjxy阅读 3,069评论 1 10