JavaScript的事件机制

为了能够与用户互动,制作交互性网页,JS必须提供事件机制。这也是DOM另一大feature

介绍

DOM事件流如下:

图片来源:JavaScript高级程序设计图13-1

事件分成两个阶段:捕获阶段从高至低;冒泡阶段从低至高
当用户点击按钮,那么先进入捕获阶段,此时事件从document->html->body
然后是冒泡阶段,事件从button->body->html->document

具体的事件虽然种类繁多,但只有两大类:
用户事件,对应用户采取了某个动作,比如onclick
时点事件,对应某个条件满足后自动触发,比如onload
更多具体内容见相关手册

注册事件

(1)html式
这种写法是最古老的方式,很简单,就是在html元素属性里定义JS脚本,比如

<input type="button" value="Order" onclick = "return doTest();"></input>

当按钮被点击时,执行onclick对应的脚本。
通常只是定义一个函数,避免属性过长。
另外这里有返回值,返回true则浏览器继续执行接下来的动作,false则终止

虽然写法简单直观,但是老式的毕竟有缺点。最主要的缺点是将脚本与页面绑定到一起了,如果将来要改变,必须改html文件。
现在已经不推荐这么写了

(2)JS属性式
html里的属性在DOM里都有对应,那么实际上通过直接赋值属性也可以起到一样的效果,比如
button.onclick = function() {return doTest();};
再比如指定脚本出错时的回调函数
window.onerror = function(msg, url, line) {alert(msg + “\n” + url + “\n” + line);}

这样的好处是把回调函数放到脚本里,html不会出现相关内容
另外也方便动态调整以及删除事件(赋值null即可)

(3)事件监听式
这个是目前最高级的方式,比前两种方式复杂但是灵活度也更高
背后的设计理念是注册事件+监听,与当前主流用户事件设计模式一样,因此应该不难理解。
var btn = document.getElementById(“mybtn”);
var fnHandler = function(e) {alert(e.target.id);}
btn.addEventListener(“click”, fnHandler, false);
首先获取目标,然后准备一个回调函数,之后调用方法addEventListener
第一个参数是事件名称,不带on
第二个参数是回调函数
第三个参数是表明在捕获阶段触发还是冒泡阶段触发。二者只能选一个,一般而言都应该在冒泡阶段,传false。false是默认参数所以可以不写
这个addEventListener可以给同一个事件注册多个回调,于是这就导致了移除监听有点麻烦
移除的话调用removeEventListener,但是注意传入的回调参数必须跟注册时候的一样,不然就失败(毕竟可以注册多个)。换言之匿名函数一旦注册就没法移除了

另外由于需要获取对象再调用方法,所以使用时必须确保该对象已经加载出来了
如果在head部分用上述代码必然失败。最好等html加载完毕再统一注册,于是可以注册onload事件。一种方式是直接在html的body加上属性onload,另一种方式是为window注册load,推荐后者。

事件对象

上述例子里回调函数是有参数的。实际上不管用哪种写法浏览器都会传入事件对象
e.type返回事件名称,e.target返回事件实际目标,e.currentTarget返回注册该事件的节点
e.preventDefault()方法会阻止浏览器默认行为,e.stopPropagation()会阻止事件继续传播
当然每种具体事件还有独有的属性和方法,见相关手册

那为什么要费那么大劲引入事件流呢?
如果给每个对象注册相应事件,则如果对象太多的话整体效率会变差,毕竟每个方法都是一个函数对象
在编译过的程序里这不是什么问题,但是JS里就是个大问题了
事件流让我们能够在高级对象注册一个方法来处理其下的许多对象
比如我们可以在body注册一个点击事件,则用户不管点击什么都会冒泡到body部分,然后可以根据事件对象进一步处理,一个方法即可
document.body.addEventListener(“click”, fnHandler, false);
不过这样处理的话,则fnHandler内部要做的工作就会多一些,通常要根据事件对象分类讨论,此时e.target就很重要了
个人感觉这样有利有弊,看情况选择合适的方法吧