JavaScript DOM(三)-DOM事件

1.简介

本文将从事件流(捕获阶段、目标阶段、冒泡阶段)、事件处理程序(HTML级、DOM0级、DOM2级、简单的兼容示例)、事件委托、模拟事件(DOM中的模拟事件、IE中的模拟事件、自定义事件)、事件对象、事件类型这几个方面来简要的介绍DOM事件。

2.事件流

事件流描述的是从页面中接收事件的顺序。
–《JavaScript高级程序设计》

事件冒泡

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

事件捕获

Netscape Communicator团队提出的另一种事件流叫做事件捕获(event capturing)。事件捕获的思想是不太具体的节点应该更早的接收到事件,而最具体的节点应该最后接收到事件。
–《JavaScript高级程序设计》

DOM事件流

“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。
–《JavaScript高级程序设计》

多数支持DOM事件流的浏览器都实现了一种特定的行为;即使“DOM2级事件”规范明确要求捕获阶段不会涉及事件目标,但IE9、Safari、Chrome、Firefox和Opera9.5及更高版本都会在捕获阶段触发事件对象上的事件。结果,就是有两个机会在目标对象上操作事件。
–《JavaScript高级程序设计》

3.事件处理程序

事件就是用户或者浏览器自身执行的某种动作,而响应某个事件的函数就叫做事件处理程序(事件侦听器)。

事件处理程序主要分为三种,分别是HTML事件处理程序、DOM0级事件处理程序和DOM2级事件处理程序,下面将分别对这三种事件处理程序进行总结分析。

HTML事件处理程序

示例代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>HTML事件处理程序</title>
    <script type="text/javascript">
        function showMessage() {
            console.log('wrapper', this, event);
        }
        function showBodyMessage() {
            console.log('wrapper', this, event);
        }
        var i = 10;
    </script>
</head>
<body onclick="showBodyMessage" id="body">
    <div onclick="showMessage()" id="wrapper">
        <div onclick="console.log('inner', this, event, i)" id="inner">
            click me!
        </div>
    </div>
</body>
</html>

分析总结:

  1. 添加监听函数
  • 1.1 有两种方式用来添加监听函数,一是在 on- 属性后直接写代码,点击后会执行;二是在 on- 属性后写一个函数。
  • 1.2 on- 属性的值是一个将要执行的代码,在将函数名添加到值的位置时,不要忘记加上圆括号。
  1. 移除监听函数。
  • 无法直接移除监听函数。
  1. 是否可以添加多个事件处理函数
  • 不可以添加多个事件处理函数。
  1. 事件传播
  • 在主流浏览器上都是:事件冒泡。
  1. this 指向
  • 直接在属性值里写代码,this指向是绑定事件的那个Element节点。
  • 在值的位置写函数的情况,this指向是全局window对象。
  1. event对象
  • 直接在属性值里写代码,可以直接使用event对象。
  • 在值的位置写函数的情况,可以在不传参的情况下直接使用,也可以在参数列表中将event对象传过去。
  1. 其他
  • 事件处理程序中的代码在执行时,有权访问全局作用域中的任何代码。
  • 通过event变量,可以直接访问事件对象,而你不用自己定义,也不用从函数的参数列表中读取。
  • HTML代码与JavaScript代码紧密耦合。所以一般不推荐这种写法。

DOM0级事件处理程序

示例代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script>
        window.onload = function() {
            var wrapperEle = document.getElementById('wrapper');
            var innerEle = document.getElementById('inner');
            innerEle.onclick = function() {
                console.log('inner', this, event);
            }
            wrapperEle.onclick = showMessage;

            function showMessage() {
                console.log('wrapper', this, event);
            }

            // 移除监听函数
            // wrapperEle.onclick = null;
        }
    </script>
</head>
<body>
<div id="wrapper" onclick="console.log('HTML 事件处理程序')">
    <div onclick="console.log('html inner click')" id="inner">
        click me!
    </div>
</div>
</body>
</html>

分析总结:

  1. 添加监听函数
  • 获取对象,将这个属性的值设置为一个函数,就可以指定一个事件处理函数。
  1. 移除监听函数
  • 在其之后,将 on- 属性的值设置为 null,即可移除事件处理程序。
  1. 是否可以添加多个事件处理函数
  • 可以添加多个事件处理程序,但是只有最后一个会被执行。
  • 如果在该节点上既有HTML事件处理程序,又有DOM0级事件处理程序,HTML事件处理程序会被覆盖,不会被执行。
  1. 事件传播
  • 在主流浏览器上为:事件冒泡。
  1. this指向
  • 绑定事件的Element节点。
  1. event对象
  • 可以传过去,即在函数参数位置写上形参。
  • 也可以不传形参直接使用,但在函数体内只能以event引用event对象。
  1. 其他
  • 如果一个节点上既有HTML级事件处理程序,又有DOM 0级事件处理程序,HTML级事件处理程序会被覆盖,不会执行。

DOM2 级事件处理程序

示例代码

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>DOM2级事件</title>
    <script type="text/javascript">
        window.onload = function () {
            // 测试捕获冒泡阶段代码
            (function () {
                var phases = ['', '捕获阶段', '目标阶段', '冒泡阶段'];
                var rootEle = document.getElementById('root');
                var wrapperEle = document.getElementById('wrapper');
                var innerEle = document.getElementById('inner');
                // 冒泡
                rootEle.addEventListener('click', function () {
                    console.log('root click', '冒泡监听');
                    console.log(this, event, phases[event.eventPhase]);
                }, false);
                wrapperEle.addEventListener('click', function (e) {
                    console.log('wrapper click', '冒泡监听');
                    console.log(this, e, phases[event.eventPhase]);
                }, false);
                innerEle.addEventListener('click', function () {
                    console.log('inner click', '冒泡监听');
                    console.log(this, event, phases[event.eventPhase]);
                }, false);
                // 捕获
                rootEle.addEventListener('click', function () {
                    console.log('root click', '捕获监听');
                    console.log(this, event, phases[event.eventPhase]);
                }, true);
                wrapperEle.addEventListener('click', function () {
                    console.log('wrapper click', '捕获监听');
                    console.log(this, event, phases[event.eventPhase]);
                }, true);
                innerEle.addEventListener('click', function () {
                    console.log('inner click', '捕获监听');
                    console.log(this, event, phases[event.eventPhase]);
                }, true);
            })();
            // 测试监听、移除监听代码
            (function () {
                var innerEle = document.getElementById('inner');
                var click1 = function () {
                    console.log(1);
                };
                var click2 = function () {
                    console.log(2);
                };
                var click3 = function () {
                    console.log(3);
                };
                innerEle.addEventListener('click', click1, false);
                innerEle.addEventListener('click', click2, false);
                innerEle.addEventListener('click', click3, false);
                // 这种方式监听的事件是无法移除监听的。
                innerEle.addEventListener('click', function () {
                    console.log(4);
                }, false);
//                innerEle.removeEventListener('click', click2, false);
                // 无法移除监听
//                innerEle.removeEventListener('click', function () {
//                    console.log(4);
//                }, false);
            })();
        }
    </script>
</head>
<body id="root">
<div id="wrapper">
    <div id="inner">
        click me!
    </div>
</div>
</body>
</html>

分析总结:

  1. 添加监听函数
  • 使用 addEventListener 函数
  1. 移除监听函数
  • 使用 removeEventListener 函数
  • 参数列表要求与 addEventListener 完全一致
  1. 是否可以添加多个事件处理函数
  • 可以,按照添加顺序执行
  1. 事件传播
  • 当指定第三个参数为 true 时,按照捕获方式调用。
  • 其他情况,为冒泡方式调用。
  1. this指向
  • 绑定事件的Element节点。
  1. event对象
  • 可以传过去,即在函数参数位置写上形参。
  • 也可以不传形参直接使用,但在函数体内只能以event引用event对象。
  1. 其他
  • 一个常见错误,匿名函数无法移除,详见示例。
  • Edge支持该事件处理程序。

IE事件处理程序(与DOM2级事件处理程序对应)

示例代码(该代码只能在ie浏览器下运行)

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>IE事件处理程序</title>
    <script type="text/javascript">
        window.onload = function () {
            var rootEle = document.getElementById('root');
            var wrapperEle = document.getElementById('wrapper');
            var innerEle = document.getElementById('inner');
            rootEle.attachEvent('onclick', function () {
                console.log('root listener 1', this, event);
            });
            wrapperEle.attachEvent('onclick', function () {
                console.log('wrapper listener 1', this, event);
            });
            innerEle.attachEvent('onclick', function () {
                console.log('inner listener 1', this, event);
            });
            rootEle.attachEvent('onclick', function () {
                console.log('root listener 2', this, event);
            });
            wrapperEle.attachEvent('onclick', function () {
                console.log('wrapper listener 2', this, event);
            });
            innerEle.attachEvent('onclick', function () {
                console.log('inner listener 2', this, event);
            });
            rootEle.attachEvent('onclick', function () {
                console.log('root listener 3', this, event);
            });
            wrapperEle.attachEvent('onclick', function () {
                console.log('wrapper listener 3', this, event);
            });
            innerEle.attachEvent('onclick', function () {
                console.log('inner listener 3', this, event);
            });
        }
    </script>
</head>
<body id="root">
<div id="wrapper">
    <div id="inner">
        click me!
    </div>
</div>
</body>
</html>

与DOM2级事件处理程序对应,ie也有两个方法分别对应添加和移除事件处理程序:

  • attachEvent()
  • detachEvent()

分析总结:

  1. 添加监听函数
  • 使用 attachEvent 函数
  • 第一个参数为’on-’,例如’onclick’,而不是DOM中的’click’。
  • 接受两个参数。因为IE8及之前的版本只支持事件冒泡。
  1. 移除监听函数
  • 使用 detachEvent 函数
  • 参数列表要求与 attachEvent 完全一致
  1. 是否可以添加多个事件处理函数
  • 可以,按照添加顺序相反的顺序执行。(IE8及以下)
  • 可以,按照添加顺序执行。(IE9及以上,不包括Edge)
  1. 事件传播
  • 冒泡方式调用。
  1. this指向
  • 指向 window 对象
  1. event对象
  • 可以传过去,即在函数参数位置写上形参。
  • 也可以不传形参直接使用,但在函数体内只能以event引用event对象。
  1. 其他
  • 同样的,匿名函数无法移除。
  • 支持IE事件处理程序的浏览器有IE和Opera。
  • Edge支持标准DOM2级事件处理程序,不支持IE事件处理程序。

跨浏览器的事件处理程序

1.《JavaScript高级程序设计》示例

var EventUtil = {
    addHandler: function(element, type, handler) {
       if(element.addEventListener){
            element.addEventListener(type, handler, false);
       } else if(element.attachEvent) {
            element.attachEvent('on' + type, handler);
       } else {
            element['on' + type] = handler;
       }
    },
    removeHandler: function(element, type, handler) {
       if(element.removeEventListener){
            element.removeEventListener(type, handler, false);
       } else if(element.detachEvent) {
            element.detachEvent('on' + type, handler);
       } else {
            element['on' + type] = null;
       }
    }
}

2.使用原型拓展

HTMLElement.prototype.addEvent = function(type, fn, capture) {
    var el = this;
    if (window.addEventListener) {
        el.addEventListener(type, function(e) {
            fn.call(el, e);
        }, capture);
    } else if (window.attachEvent) {
        el.attachEvent("on" + type, function(e) {
            fn.call(el, e);
        });
    } 
};

注:
示例2来源于张鑫旭大神的文章漫谈js自定义事件、DOM/伪DOM自定义事件

4.事件委托

在JavaScript中,添加到页面上的事件处理程序将直接影响到页面的整体运行性能。首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的DOM访问次数,会延迟整个页面的交互就绪事件。
–《JavaScript高级程序设计》

对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
–《JavaScript高级程序设计》

使用场景:
在一个父节点下,有很多个子节点,需要为所有的子节点添加点击事件。一般情况下,我们会为所有的元素添加点击事件,就会有很多个重复的事件处理程序。现在我们使用事件委托,只需在DOM树中合适的节点上添加一个事件处理程序就可以了。

示例程序

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>事件委托</title>
    <script type="text/javascript">
        window.onload = function () {
            var ulEle = document.getElementById('wrapper');
            ulEle.onclick = function () {
                console.log('ul DOM0');
            };
            ulEle.addEventListener('click', function (e) {
                console.log(this, e.target, e.currentTarget);
                // 目标节点
                var target = e.target;
                var dataID = target.getAttribute('data-id');
                console.log(dataID);
            }, false);
        }
    </script>
</head>
<body>
<ul id="wrapper">
    <li data-id="a">aaaaaaaaa</li>
    <li data-id="b">bbbbbbbbb</li>
    <li data-id="c">ccccccccc</li>
    <li data-id="d">ddddddddd</li>
    <li data-id="e">eeeeeeeee</li>
</ul>
</body>
</html>

如果可行的话,也可以考虑为document对象添加一个对象处理程序,用以处理页面上发生的某种特定类型的事件。
优点如下所示:

  • document对象很快就可以访问,而且可以在页面生命周期的任何时点上为它添加事件处理程序(无需等待DOMContentLoaded或load事件)。
  • 在页面中设置事件处理程序所需的时间更少。
  • 整个页面占用的内存空间更少,能够提升整体性能。
    –《JavaScript高级程序设计》

个人而言,我比较推荐在合适的节点添加事件委托,除非有必要或者事件比较容易处理,否则不建议将所有事件都委托到document对象上。

事件委托的优点:

  • 减少了声明的事件总数,减少了所需内存。
  • 能够对添加事件处理程序之后再添加的节点进行处理,而不需要对后来添加的节点再次添加事件处理程序。

分析总结:

  • this指向,指向事件绑定的节点,即父节点。

5.模拟事件

我们可以使用JavaScript在任意时刻来触发特定的事件,而此时的事件就如同浏览器创建的一样。

DOM中的事件模拟

第一步,使用document对象的createEvent方法创建event对象。该方法接受一个参数,即表示要创建的事件类型的字符串。
第二步,在创建了event对象之后,还需要使用与事件有关的信息对其进行初始化。不同类型的这个方法的名字也不相同,具体要取决于createEvent方法中使用的参数。
第三步,使用dispatchEvent方法触发事件,所有支持事件的DOM节点都支持这个方法。

IE中的事件模拟

第一步,使用document.createEventObject()方法可以在IE中创建event对象。但与DOM方式不同的是,这个方法不接受参数,结果会返回一个通用的event对象。
第二步,必须手工为这个对象添加必要的信息(没有方法来辅助完成这一步骤)。
第三步,就是在目标节点上调用fireEvent()方法,这个方法接受两个参数:事件处理名称和event对象。在调用fireEvent方法时,会自动为event对象添加SRCElement和type属相,其他属性都必须通过手工添加。
换句话说,模拟任何IE支持的事件都采用相同的模式。

自定义事件

DOM3级还定义了“自定义事件”。自定义事件不是由DOM原生触发的,它的目的是让开发人员创建自己的事件。
要创建新的自定义事件,可以调用createEvent(‘CustomEvent’)。返回的对象有一个名为initCustomEvent()的方法,接受如下四个参数。

  • type (字符串):触发的事件类型。
  • bubbles (布尔值):表示事件是否应该冒泡。
  • cancelable (布尔值):表示事件是否可以取消。
  • detail (对象):任意值,保存在event对象的detail属性中。

一个简单的示例

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>自定义DOM事件</title>
    <script type="text/javascript">
        window.onload = function () {
            var event = document.createEvent('CustomEvent');
            event.initCustomEvent('dbElementClick', true, false, '点击了两个子节点');
            var outerEle = document.getElementById('outer');
            var wrapperEle = document.getElementById('wrapper');
            outerEle.addEventListener('dbElementClick', function (e) {
                console.log('outer', e.detail);
            }, false);
            wrapperEle.addEventListener('dbElementClick', function (e) {
                console.log('wrapper', e.detail);
            }, false);
            var firstEle;
            outerEle.addEventListener('click', function (e) {
                var target = e.target;
                if (!firstEle) {
                    firstEle = target
                } else if (target !== firstEle) {
                    outerEle.dispatchEvent(event);
                }
            }, false);
        }
    </script>
</head>
<body id="wrapper">
<div id="outer">
    <div id="click1">
        click me!
    </div>
    <div id="click2">
        click me!
    </div>
</div>
</body>
</html>

关于模拟事件和自定义事件,更详细的信息请参考阮一峰老师的文章或者《JavaScript高级程序设计》这本书。

6.事件对象(event)

阻止事件默认行为

event.preventDefault()
event.cancelable;
event.defaultPrevented;

其中

  • preventDefault() 用来阻止事件的默认行为。
  • cancelable 表示事件是否允许被取消。
  • defaultPrevented 表示是否调用过 preventDefault() 方法。
  • 监听事件 return false 会起到和 preventDefault() 方法一致的效果。

阻止事件传播

event.stopPropagation()
event.stopImmediatePropagation()

其中

  • stopPropagation() 方法用于阻止事件在DOM上的传播,捕获或者冒泡阶段均可调用。
  • stopImmediatePropagation() 有两种作用,其一该方法可以阻止事件在DOM上的传播,类似于 stopPropagation() 方法。其二,如果对一个节点定义了多个事件监听函数,那么事件监听函数将会按照顺序依次执行,使用该方法后,之后的事件处理程序都不会被触发。

事件节点

event.currentTarget
event.target

其中

  • event.currentTarget 属性指向正在执行的监听函数绑定的节点。
  • event.target 属性指向触发事件的节点,即事件最初发生的节点。

更多细节请参考阮一峰老师的文章事件模型或《JavaScript高级程序设计》

最后,给出这一小节的示例程序

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>DOM 事件</title>
    <script type="text/javascript">
        window.onload = function () {
            // 阻止事件默认行为
            (function () { // 请先注释该函数下面的语句
                var open = document.getElementById('open');
                open.addEventListener('click', function () {
                    event.preventDefault();
                }, true);
            })();
            // 阻止事件传播
            (function () {
                var wrapperEle = document.getElementById('wrapper');
                var innerEle = document.getElementById('inner');
                wrapperEle.addEventListener('click', function () { // 只有这个事件会被触发
                    event.stopPropagation();
                    console.log('wrapper 捕获');
                }, true);
                innerEle.addEventListener('click', function () {
                    console.log('inner 捕获');
                }, true);
                wrapperEle.addEventListener('click', function () {
                    console.log('wrapper 冒泡');
                }, false);
                innerEle.addEventListener('click', function () {
                    console.log('inner 冒泡');
                }, false);
            })();
            (function () {
                var wrapperEle = document.getElementById('wrapper');
                var innerEle = document.getElementById('inner');
                wrapperEle.addEventListener('click', function () { // 这个事件会被触发
                    console.log('wrapper 捕获1');
                }, true);
                wrapperEle.addEventListener('click', function () { // 这个事件会被触发
                    event.stopImmediatePropagation();
                    console.log('wrapper 捕获2');
                }, true);
                wrapperEle.addEventListener('click', function () {
                    console.log('wrapper 捕获3');
                }, true);
                innerEle.addEventListener('click', function () {
                    console.log('inner 捕获');
                }, true);
                wrapperEle.addEventListener('click', function () {
                    console.log('wrapper 冒泡');
                }, false);
                innerEle.addEventListener('click', function () {
                    console.log('inner 冒泡');
                }, false);
            })();
        }
    </script>
</head>
<body>
<div id="wrapper">
    <div id="inner">
        click me!
        <a id="open" href="https://www.baidu.com/" onclick="return false;">百度</a>
    </div>
</div>
</body>
</html>

7.事件类型

由于事件类型繁多,本文不再赘述,详细信息请参考阮一峰老师的文章事件种类或《JavaScript高级程序设计》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值