事件

BOM和DOM都是用来控制页面的表现形式,是一种单向展示给用户的方式,只有引入了事件,才可以说是交互式的页面。

事件的类型比较复杂,有传统的DOM事件,还有DOM2级事件,BOM也有事件,API非常繁琐,但无论如何,事件的处理流程和原理必须清楚。

事件流的概念

事件流描述的是从页面接收事件的顺序。基础的事件流分为两种:事件冒泡 和 事件捕获。现代浏览器只需要使用事件冒泡即可。

DOM2级事件则规定的比较复杂,其事件流有三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。

事件流是概念上的模型,其实事件就是浏览器或者用户执行的行为,响应某个事件的函数就是事件处理程序。简单的说,只要为事件指定事件处理程序,在行为发生的时候就会触发JS代码的执行,从而对这个行为进行响应,用户就感觉和页面会有交互。

为事件指定处理程序的方式有三种:

  1. HTML事件处理程序

    HTML事件处理程序是指在HTML标签内部,通过指定onclick之类的属性来指定,属性的值是可以被执行的JS字符串代码,或者是在页面其他地方定义好的函数。

    这种事件处理程序,其内部有一个局部变量event用于访问事件对象,this等于事件的目标元素(onclick所在的元素节点)

    事件处理程序的缺点是加载的时候可能还不具备条件执行代码,以及耦合程度太高,混杂了页面结构与行为,所以不推荐使用该方法,就和不推荐使用CSS的内联样式一个原理。

  2. DOM 0级事件处理程序

    这种事件处理程序,就是通过JS给目标元素绑定事件处理程序,不写死在HTML标签里,而是动态的设置类似onclick这种属性为JS代码来执行。

    用这种方法绑定的代码,被认为是在目标元素的作用域中执行,所以其内部this代表当前元素。

    如果想取消事件,就将事件响应函数的属性值设置成null

    DOM 0级事件处理程序是普通web页面常用的做法,有效的降低了JS代码与HTML的耦合程度。但缺点是依然采用具体属性,而且只能绑定一个事件处理程序。

  3. DOM 2级事件处理程序
    这是比较好的添加事件方法,不再局限于操作标签属性,还可以添加多个事件处理程序。有两个主要方法:

    • .addEventListener(event,handler,boolean)
    • .removeEventListener(event,handler,boolean)

    第一个参数是事件的名称,比如”click”,第二个参数是函数名或者函数代码,第三个参数如果是true,表示在捕获阶段调用事件处理程序,如果是false,表示在冒泡阶段调用事件处理程序

    这两个方法在使用前都需要先定位元素,addEventListener(event,handler,boolean)用于给目标元素绑定事件,可以反复调用该方法来绑定多个事件,事件会按照绑定的顺序触发。

    removeEventListener(event,handler,boolean)用于给目标元素去除事件,只能去掉由addEventListener(event,handler,boolean)添加的事件。由于必须传入函数名,所以addEventListener(event,handler,boolean)给目标元素添加的匿名事件处理程序无法去除。

    关于最后一个参数的问题。一般都是在冒泡阶段处理事件,除非明确的知道确实要在捕获阶段就处理事件,否则都将事件放到冒泡阶段处理即可。

从这三种模式来看,最优先使用的应该还是DOM 2级的事件处理程序方式,比较清晰而且一一对应。如果DOM 2级不支持则使用DOM 0级。除非简单的单页面应用,不要使用HTML事件方式。

event 事件对象 

在刚才的事件处理程序中,已经提到了事件处理程序内部可以用event来访问事件对象。这个对象包含着所有与事件有关的信息,比如导致事件发生的元素,事件的类型,鼠标位置,键盘按键等。所有浏览器都支持event对象。

只要一个函数被作为事件处理程序,在事件触发的时候,浏览器都会把event对象传入函数内部作用域,可以在事件处理程序中访问。

event 对象的共通属性
方法或属性名 类型 读/写 说明
bubbles Boolean 只读 事件是否冒泡
cancelable Boolean 只读 是否可以取消事件的默认行为
currentTarget Element 只读 当前处理事件的元素。比如用冒泡处理的话,就返回实际冒到的那一个事件处理元素
defaultPrevented Boolean 只读 为true表示调用了preventDefault()(DOM 3级事件)
eventPhase Boolean Integer 表示调用事件处理程序的阶段,1表示捕获阶段,2表示处于目标,3表示冒泡阶段
preventDefault() Function 只读 如果cancelale的属性是true,则可以调用该方法来阻止事件的默认行为。
stopImmediatePropagation() Function 只读 取消事件的进一步捕获或冒泡,阻止其他事件处理程序被调用。比如绑定了多个事件处理程序,执行过这个方法之后,之后的事件处理程序都不工作。
stopPropagation() Function 只读 取消事件进一步捕获或冒泡,如果bubbles为true可以使用这个方法。和上一个类似,只阻止冒泡,不阻止当前之后的事件处理程序。
target Element 只读 事件的目标,也就是事件实际发生地,后文详述
trusted Boolean 只读 表示事件是否是浏览器生成的,如果为false表示事件是有JS代码生成的(DOM 3级事件新增)
type String 只读 事件的类型
view AbstractView 只读 表示事件所发生在的视图,也就是事件发生的页面的window对象。

这里需要详细解释的是两个属性 targetcurrentTarget 以及两个方法stopImmediatePropagation()stopPropagation()。为了测试,先建立一个div->p->button的页面。

<div id = 'div' style="border: black solid 1px">我是DIV。
    <p id="para">我是P元素
         <button id="button" type="button">我是按钮</button></p>
</div>

然后建立一个事件处理程序,同时绑定到三个元素上。

div = document.getElementById("div");
para = document.getElementById("para");
button = document.getElementById("button");
div.addEventListener('click',test,false);
para.addEventListener('click',test,false);
button.addEventListener('click',test,false);
function test(event) {
    console.log(this);
    console.log(event.currentTarget);
    console.log(event.target);
}

此时点击按钮,可以发现console里边打印了9行内容,这是因为事件从button元素一直冒泡到div元素,其中每个元素都有事件处理程序,就处理了事件。

经过观察可以发现:this和currentTarget的值每次都在变化,事件处理程序绑定在哪里,这两个变量就指向哪里。而target的值没有变化,始终指向button,也就是事件发生的地方。

再换一下点击的位置,点击按钮右侧的空白地方,也就是P标签,会发现这个时候只显示了6行内容,说明事件发生在p标签,然后向上冒泡。

事件冒泡的思路带来一个启发,就是如果需要监听一批同类型子元素的事件,不需要给所有子元素都绑定事件处理程序,只需要在其父元素或共同某个祖先元素上绑定事件处理程序用来侦听即可,一样可以通过target拿到实际发生事件的对象。这种思路就是事件委托

再来看一下两个方法,修改JS代码为如下,为每个标签绑定两个事件。

div = document.getElementById("div");
para = document.getElementById("para");
button = document.getElementById("button");

div.addEventListener('click',first,false);
div.addEventListener('click',second,false);
para.addEventListener('click',first,false);
para.addEventListener('click',second,false);
button.addEventListener('click',first,false);
button.addEventListener('click',second,false);
function first(event) {
    console.log("这是第一个事件,来自于:"+event.currentTarget.tagName);
    event.stopPropagation();
}

function second(event) {
    console.log("这是第二个事件,来自于:"+event.currentTarget.tagName);
}

点击后可以发现,无论点击哪一个元素,都只运行了所点击的元素上的两个事件,取消了事件继续向上冒泡。

再来将first事件内的 stopPropagation(); 换成 stopImmediatePropagation()点击看一看,发现不仅不冒泡了,还只运行了第一个事件,这就是这个函数的功能。

刚才是针对同一个元素绑定了多个事件处理程序,实际开发中并不会为一个事件绑定多个事件处理程序,给一个元素绑定多个事件处理程序是用来处理不同类型的事件的,这个时候就需要判断event.type的值来进行不同的事件处理。例如:

button = document.getElementById("button");
button.addEventListener('click',handler,false);
button.addEventListener('mouseover',handler,false);
button.addEventListener('mouseout',handler,false);

function handler(event) {
    event.stopImmediatePropagation();
    switch (event.type) {
        case "click":
            event.currentTarget.style.color="red";
            break;
        case "mouseover":
            event.currentTarget.style.backgroundColor="green";
            break;
        case "mouseout":
            event.currentTarget.style.backgroundColor="red";
            event.currentTarget.style.color="";
            break;
    }
}

事件类型

事件的类型非常多,基本上所有的浏览器都支持DOM 3级事件,可见浏览器也是在用户交互上的支持最广泛。

为了避免变成罗列API,将事件做了一些大的区分,然后在实际用到的时候再来看。

事件列表与简介
事件分类 事件类型 说明
UI事件 load 页面加载完在window对象触发,图片加载完在img上触发,嵌入内容加载完在对应的元素上触发。注意onload事件虽然是页面装载完,但实际上是在window对象上触发事件。
unload 页面切换的时候发生,用的最多的方法就是清除一些变量,防止内存泄露。可以给window对象绑定该方法,或者设置body标签设置onunload属性。
resize 浏览器窗口调整新高度或宽度的时候触发该事件,在window对象上触发,也可以设置到body对象上。
scroll 页面滚动的时候发生,发生在window对象上,可以通过body元素的.scrollLeft和.scrollTop来监控。这个事件设置了以后页面只要滚动就会触发很多次,要注意性能。
焦点事件 blur 元素失去焦点时发生,这个事件不会冒泡,所有浏览器都支持。
focus 元素获得焦点的时候发生,这个事件也不会冒泡
focusin 获得焦点时触发,是focus的通用版本,这个会冒泡
focusout 失去焦点时触发,是blur的通用版本,这个会冒泡
鼠标事件 click 单击左键或者按下enter键触发
dblclick 双击鼠标左键触发,不能通过键盘触发
mousedown 按下任意鼠标按钮触发,不能通过键盘触发
mouseenter 鼠标从元素外部移动到元素内部的时候触发,不冒泡,在鼠标移动到后代元素上不触发。
mouseleave 位于元素范围内的鼠标移动到元素范围外的时候触发,这个事件也不冒泡,鼠标移动到后代元素上也不触发。
mousemove 当鼠标在元素内部移动的时候反复的触发,不能通过键盘触发该事件。
mouseout 鼠标位于一个元素上方,然后用户将其移入另一个元素时触发,移入的元素可能位于外部也可能位于内部(子元素)
mouseover 鼠标位于一个元素外边,移动到另一个元素编辑内触发,不能通过键盘触发。
mouseup 释放鼠标按钮的时候触发。不能通过键盘触发
除了不冒泡的事件之外,鼠标事件都冒泡,还可被取消默认行为,也就是无法点击。只有在同一个元素上相继触发mousedown和mouseup,才会触发click事件,如果这两个其中一个被取消,click就触发不了。如果有代码连续两次阻止click,那dblclick就无法触发。鼠标还有位置等事件,在需要用到的时候再学习。
键盘事件 keydown 按下键盘任意键触发,如果按住不放,会反复触发该事件。
keypress 按字符键和Esc键会触发该事件。
keyup 释放键盘上的键触发。所有元素都支持上边三个事件,但一般只有文本框才用得到
textInput 虽然不是键盘事件,但是通常用于文本框,这个会在文本插入文本框之前触发。
按一个键的时候,先触发keydown,然后是keypress,之后释放键是keyup。如果在文本框里输入,则在keydown和keypress之后,会先触发textInput(事件名里的I要大写)事件,之后才是keyup事件。如果不是按字符键和Esc键,keypress事件不会触发。对于键盘事件可以从event.keyCode中获取是按下了哪一个键。

其他的事件如滚轮事件,复合事件,变动事件,设备事件和HTML 5 事件在需要用到的时候再来看。最后再看两个事件处理思路:

事件委托

<ul id="ul1">
    <li id="li1">第1个元素</li>
    <li id="li2">第2个元素</li>
    <li id="li3">第3个元素</li>
    <li id="li4">第4个元素</li>
</ul>

<script>
    ul = document.getElementById("ul1");
    ul.addEventListener("click",handle,false);

    function handle(event) {
        event.stopImmediatePropagation();
        if(event.target.tagName=== "LI"){
            console.log('执行处理来自'+ event.target.id+'的单击');
        }else{
            console.log("单击了ul元素,不进行操作");
        }
    }
</script>

给ul元素绑定了事件,用于统一管理对li元素的点击,其中先停止了事件继续冒泡,然后判断是否点击来自LI标签。如果是再进行操作,如果不是就不进行操作

模拟事件

之前的事件,都是浏览器监听用户的操作形成的事件,还可以让浏览器模拟事件的发生,之后事件就会按照正常的程序被得到处理。这一技术经常用在Web测试中。

事件模拟的方法是先通过 document.createEvent(type) 来建立一个事件对象,之后通过该事件的初始化函数传入参数设定这个事件,最后用 element.dispatchEvent(event) 方法触发事件。

模拟事件对应关系
事件类型字符串 初始化函数
UIEvent 无初始化,不使用该方法,采用更具体的MouseEvent和KeyboardEvent
MouseEvent event.initMouseEvent(args)
KeyboardEvent event.initKeyEvent(args)
MutationEvent event.initMutationEvent(args)
CustomEvent event.initCustomEvent(args)

这里的事件类型字符串就是传给document.createEvent(type)的参数。最后一个custom是指的自定义事件。了解即可,如有需要再仔细学习。

总结

事件可以说是页面交互的核心,目前还仅在页面内操作,在加上后端的知识之后,还可以根据事件与后端通信,极大的扩展页面的交互性。

事件的内容虽然很多,但核心还是理解事件流中的冒泡模式,然后依照自己想要的结果,采取事件委托的理念,合理的设置事件操作DOM。至于具体事件实现,了解常用的键鼠事件,其他事件可以遇到后慢慢研究。