DOM编程之事件(二)

记忆中某天在某个个人网站上看见一个非常酷炫的效果:整个页面都是萤火虫似的小光点组成的LOGO,鼠标滑过的时LOGO被打散,屏幕上泛起阵阵涟漪,鼠标停止又变换图形。当时非常震撼,甚至于忘了保存链接。这就成了我现在正在奋斗的一个目标。正巧刚看完客户端JS关于事件处理的部分,下面简单整理了一下JS中关于事件及相关的一些知识点。

<!--more-->

更新与2017-3-13,最近一直在补习JavaScript基础知识,前两天整理和完善了DOM节点(当然肯定还漏掉了很多地方),今天开始理清事件,之前的整理现在回头来看确实是太片面了。

1. 基础概念

当文档、浏览器、元素或与之相关的对象发生某些有趣的事情时,浏览器就会产生事件(也可以说成是浏览器通知应用程序发生了什么事情),书面语称作异步事件驱动编程模型。来看一个最基本的例子:

<button id="t">click</button>
<script type="text/javascript">
    t.onclick = function(e){
        console.log(e);
    }
    document.onclick = function(){
        console.log("click");
    }
</script>

代码的含义是:当点击按钮时,控制台会输出一个值,然后会输出"click"。通过这段简单的代码,我们可以得到下面线索:

  • 触发的是一个叫做"点击"的事件
  • 我们点击了一个button元素节点
  • button被点击的时候会执行一个函数
  • 匿名函数的参数e是有值的,而不是undefined
  • 通过为节点的的'onclick'属性赋值来绑定事件和回调函数
  • 点击button的时候,也触发了document的点击事件
  • 如果把button元素换成a标签,则点击的时候会跳转,仿佛一切都没有发生。

上面这七条(硬是凑出来的),分别对应了DOM事件接口中七个关键的概念,他们分别是:事件类型,事件目标,事件处理函数,事件对象,事件绑定,事件传递,默认事件操作。 这几个概念将贯穿整个DOM事件的学习,下面就让我们来详细地了解DOM事件到底是怎么一回事。

2. 事件类型

事件类型是一个表明发生事件的类型的字符串,早期的Web只支持诸如loadclick之类的简单事件,随着Web的发展,所支持的事件数量正在逐步增加,可以将事件进行分类:

  • 设备输入(移入)事件:鼠标(mouseout,mouseover,touchmove等)和键盘(keydown,keyup
  • 用户界面事件:表单(focus,blur,submit等)
  • 状态变化事件:网络(loadstart,progress等)、浏览器(load)
  • 特定API事件,如HTML5新增的dragstart等拖拽相关事件,video元素的playing等事件

由于现在事件类型数量太多了,这里就不一一列举出来了,想要了解更多可以移步HTML DOM 事件,这里将事件的分类划分的很详细。

实际上工作中最常用的事件有:

  • 处理鼠标键盘或触摸(主要用于用户交互和动画)
  • HTML表单事件(进行表单验证,JavaScript最原始的任务)
  • Window对象的事件(监听和操作浏览器)

3. 事件对象

事件对象是与特定事件类型相关且包含有关该事件详细信息的一个对象,每个事件类型都为其所对应的事件对象定义了一组属性:比如鼠标事件的相关对象包含了鼠标指针的坐标,而键盘事件包含按键的编码信息,滚轮事件包含了滚轮的转动信息等。

事件对象与事件类型是紧密相连的,这里也只能简单介绍事件对象的概念,而针对具体事件的事件对象相关属性,还是查文档比较实际(另外,控制台也是一个方便查看事件对象属性的好地方哦)。

关于事件对象还有一个问题:事件对象的获取存在着严重的兼容问题

  • 在ie8前以前,event是一个内置全局对象(但是必须在一个事件处理函数里才有内容)
  • 而在Chrome,Firefox等标准浏览器中,事件对象将作为参数传递给事件处理程序

解决这个问题的办法是在事件处理函数中进行判断(请放弃IE吧~)

function clickBtn(e){
    e = e || window.event
}

4. 事件目标

事件目标是指与发生的事件相关联的节点对象,通俗点讲就是事件发生的“地点”,描述一个事件,需要表明它是在哪个事件目标(小至一个按钮、大到整个文档)上发生的。最常见的事件目标就是Window,Document和Element元素。 我们可以通过事件对象的target属性来获取相应的事件目标。

function clickBtn(e){
    console.log(e.target);
}

5. 事件处理程序

事件处理程序指的我们提前编写的某个事件发生时所运行的程序,当事件触发时,就会执行这段代码。而另外在某些时候我们需要移除事件目标上的事件处理程序。注册事件处理程序有两种办法:

  • 在事件目标的相关属性注册处理程序
  • 将事件处理程序作为参数传递个事件目标的方法

相应地,注销事件处理程序也有与注册相对应的两种形式。

5.1. 通过属性注册和注销

注册

通过指定属性注册事件处理程序也分为两种方法,一种是通过设置标签的属性实现,一种是通过设置元素节点的属性实现,属性名是"on" + 事件名称

// 设置标签的属性
// 注意这里应该是整个函数的主体而不是函数声明
<button id="t" onclick="clickBtn()">click</button>

<script type="text/javascript">
    function clickBtn(){
        console.log("click");
    }

    // 设置元素节点的属性
    t.onclick = clickBtn;
</script>

通过指定属性注册事件处理程序的缺点在于:每个事件目标对于每种事件类型最多只有一个事件处理程序

  • 重复的节点属性赋值会覆盖先设置的属性,只有最后一个赋值生效
  • 重复的标签属性赋值会被浏览器忽略,只有第一个属性生效
  • 同时采用这两种方式注册事件处理程序,则只有节点属性赋值的方式生效。

另外,在HTML标签上绑定事件处理程序时还必须注意事件对象的问题:如果需要使用事件对象,则必须显式地传入window.event,因为标签属性值应该是处理函数的主体而不是函数的声明。

onclick="clickBtn(window.event)"

考虑到结构和行为分离的原则,实际开发中最好不要采用这种通过设置标签属性绑定事件处理程序的方法。

注销 如果需要注销通过属性注册的事件处理程序,我们只要将对应的属性值重置为空即可。

// 注销HTML标签属性的事件处理程序
t.setAttribute("onclick", "");

// 注销节点属性的事件处理程序
t.onclick = null;

5.2. 通过方法注册和注销

注册 任何能成为事件目标的对象都定义了一个addEventListener的方法,使用该方法可以为调用对象注册事件处理函数。

t.addEventListener("click",function(){
    console.log("click");
},false);

第一个参数是事件类型,第二个参数是事件处理程序,第三个参数是一个布尔值(默认为false),与事件传播相关(马上就会提到)。通过该方法可以更直观地为事件目标注册事件处理函数,且该方法所注册的事件处理函数均会执行,且不会影响通过属性绑定的事件处理程序。

function clickBtn(){
    console.log(1);
}

t.addEventListener("click",function(){
    console.log(2); //2
},false)
t.addEventListener("click",clickBtn,false); // 1

// 但是需要注意的是如果处理函数相同,则只会执行一次
// t.addEventListener("click",clickBtn,false); // 这里不会再执行

一个严重的问题是:在IE9之前是不支持addEventListener的,取而代之的是一个名为attachEvent的方法。两个方法最主要的区别在于:

  • 前者的第一个参数是事件类型,而后者的第一个参数是"on" + 事件类型的字符串
  • IE事件模型不支持事件捕获(别急,马上就到了),因此后者没有第三个参数
  • 前者绑定相同的处理函数只执行一次,而后者的调用次数与注册次数相同。
t.attachEvent("onclick",clickBtn); // 执行
t.attachEvent("onclick",clickBtn); // 也会执行

注销 如果需要移除通过方法注册的事件函数,与addEventListener相对应的是removeEventListener,与attachEvent相对应的是detachEvent

t.removeEventListener("click",clickBtn,false);

// IE8及以下
t.detachEvent("onclick",clickBtn);

需要注意的是如果是使用attachEvent,则注册了多少次就必须注销多少次。另外,这种方法并不能移除通过 属性绑定的事件处理程序。

5.3. 事件函数的执行环境

5.3.1. 事件对象作为参数

前面已经提到,通常在调用事件处理程序的时候,会把事件对象作为他们的一个参数(通过标签属性设置时需要显式指定window.event)。

5.3.2. this指向

另外一个比较重要的问题是事件处理函数中的this指向:

  • 在HTML标签上设置属性绑定的事件处理程序,其值可以是一个全局函数,也可以是某个全局变量(或其某个属性)的方法。如果是全局函数调用,其this值指向Window对象;如果是某个对象的方法调用,其this指向该对象
  • 在事件目标上设置元素属性注册的事件处理程序,可以看作是想事件目标添加了一个方法,其this就指向该事件目标本身
  • 作为闭包传递给addEventListener方法的事件处理程序,其this也是指向该事件目标的(这里应该是addEventListener内部进行了apply等处理),尤其需要注意这一点,与熟悉的的作用域有点区别
  • 作为闭包传递给attachEvent方法的事件处理程序,其this值是Window对象

5.3.3. 作用域

事件处理函数从词法上讲也是作用域,即,函数的作用域是在定义时而不是调用时决定的,同时也能操作作用域链中的变量。

但是,通过犀牛书了解到,通过HTML标签上设置属性绑定的事件处理程序是一个例外,他们被转换成能存取全局变量的顶级函数而不是任何本地变量。换句话说,这些事件处理函数运行在一个修改后的作用域链中,他们能像使用本地变量一样使用事件目标,容器(form,如果存在)对象个Document对象。

如果事件处理函数作为某个命名空间的方法调用,则其this会指向该命名空间对象;如果作为全局函数调用,this指向window。下面的例子简单表示了通过标签属性绑定的事件处理函数的作用域链,总之,最好还是不要这样做吧。

<form action="">
    <button id="t" type="button" onclick="test.clickBtn(window.event)">click</button>
</form>
<script type="text/javascript">
    a = 1;
    var test = (function(){
        var a = 100;
        var obj = {
            clickBtn(e){
                console.log(e.target);
                console.log(this.a);
                console.log(document.forms);
            },
            a: 10
        };

        return obj;
    })();
    // 将事件处理程序作为全局函数调用的,更改上面的属性值为clickBtn
    // let clickBtn = test.clickBtn;
</script>

6. 事件传播

当点击一个按钮的时候,从按钮的角度来看,我们点击的是它,但是,按钮本身位于文档中,点击按钮,必定会触发整个文档的点击事件。也就是说,文档中发生的事件并不是独立的,而是会传播的。

当一个元素接收到事件的时候,会把他接受到的所有事件传播给他的父级,一直到顶层window,这就是事件冒泡机制,如果给父类加上了事件处理函数,那么冒泡机制传递的子级事件也会被执行。

事件传播有事件捕获和事件冒泡两种形式,他们决定了事件处理函数的调用顺序,可以想象成作用力与反作用力类似,下面来看一看具体的事件传播。

6.1. 事件捕获

当触发了一个事件之后,事件从最外层的window对象开始向document对象传播并到达至最内层的事件目标为止,并调用在传播路径上的每个对象的事件捕获处理函数(如果存在的话),这就是事件捕获过程。只能通过addEventListener()且将第三个参数设置为true的方式才能注册事件捕获处理函数;而通过事件目标属性注册的事件函数均为事件冒泡处理函数。事件捕获在IE8之前的浏览器不被支持。 事件捕获模型提供了在事件没有被送达目标之前查看他们的机会,能用于调试。

6.2. 事件冒泡

当最内层事件目标调用其事件处理函数之后,事件将从事件目标沿着DOM树传播至根部,并调用在传播路径上的每个对象的事件冒泡处理函数。focus,blur,scroll,load,change等事件不会冒泡(至于其他不会冒泡的事件待收集)。冒泡机制被所有浏览器支持。

事件冒泡模型在为大量单独元素上注册处理程序提供了解决方案(在其公有祖先元素上注册事件,即事件委托)。事件委托就是事件目标不直接处理事件,而是委托其父元素或者祖先元素甚至根元素(document)的事件处理函数进行处理。可以通过事件对象的target属性获得真正触发事件的引用。事件委托是建立在冒泡模型之上的。

有时候,我们只想触发事件目标的事件,禁止事件的传播,这个需求可以通过在事件处理函数中,调用事件对象的stopPropagation方法来实现。调用该方法之后,该事件目标的任何父元素对象上的相应事件处理函数都不会被执行。该方法可以在事件传播期间的任何时间调用,它能工作在捕获期阶段,事件目标本身和冒泡阶段。

需要注意的是该方法只能在支持addEnentListener方法的浏览器上实现,且addEnentListener的第三个参数的作用,就是将事件处理程序绑定到事件捕获阶段或者事件冒泡阶段执行。

至于IE9之前的IE浏览器,取而代之的是通过设置事件对象的一个cancleBubble属性为true来阻止冒泡传播的。

7. 默认事件取消

所谓的默认事件,就是事件发生时浏览器自己默认会做的事情,即不同的事件目标在触发某些特定的事件时,会执行相关的默认操作:比如,submit按钮点击时表单提交,a链接点击时URL变化。在某些时候,我们需要取消掉浏览器的默认事件。根据注册事件处理函数的方式:

  • 在HTML标签上通过return false来取消默认事件
  • addEventListener方法注册的事件处理函数中,调用事件对象的preventDefault方法取消事件目标的默认事件
  • attachEvent方法注册的事件处理函数中,设置时间对象的returnValue属性取消事件目标的默认事件
<button id="t" type="submit" onclick="clickBtn(); return false;">click</button>

<script type="text/javascript">

    t.addEventListener("click",function(e){
        e.preventDefault();
    },false)

    // IE9之前
    t.attachEvent("onclick",function(){
            window.event.returnValue = false;
        });
</script>

在某些时候,更通用的做法是同时取消默认事件并且阻止事件传播,jQuery中在事件处理程序中使用return false就可以做到,但是这里需要注意,原生的DOM中,这种方式只能取消事件目标的默认事件操作。

8. 总结

事件是前端中一个非常重要的知识点,这里只是简单整理了事件的相关概念。实际上,不同的事件目标上也可以触发不同的事件类型,不同的事件类型所侧重的事件对象不同,因此,相关的知识还需要进一步深入学习整理才行。