深入理解JavaScript 事件

  • 本文总结自《JavaScript高级程序设计》以及自己平时的经验,针对较新浏览器以及 DOM3 级事件标准(2016年8月),对少部分内容作了更正,增加了各种例子及解析。
  • 如无特殊说明,本文后的文字引用和图片引用均来自《JavaScript高级程序设计》,引用稍有改变原文,不改变意思。
  • 本文仅作巩固基础之用,如果有不正确的地方,还望指出。
  • 更好的排版可以点这里

事件

个人认为:不论是浏览器自带的事件,还是自定义事件,都是观察者模式的实现。更确切地说:事件流是会流动的,流到哪个节点,事件在哪里发生,事件发生时,节点便会调用在这个节点绑定的事件处理程序。节点是被观察者,事件处理程序是观察者,当事件流流到被观察者时,被观察者会对外宣称“我这里发生了某个事件”,即通知观察者,也就是节点调用事件处理程序。事件流是不知道被观察者有多少个的,所以即使是0个,事件流也会继续流,流到节点时,节点会遍历自己注册的事件处理程序,存在就调用。具体浏览器的实现和优化肯定更加复杂和精妙,但原理应该是这样(以上为个人理解)。

事件流

事件流分为事件冒泡和事件捕获:

  • 如果你把手指放在圆心上,那么你的手指指向的不是一个圆,而是纸上的所有圆。在浏览器上单击按钮的同时,你也单击了按钮的容器元素,甚至也单击了整个页面。事件流描述的是从页面中接收事件的顺序
  • IE开发团队提出了事件冒泡流、Netscape开发团队提出了事件捕获流。

事件冒泡

  • 事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点,所有现代浏览器都支持事件冒泡,除IE5.5外,均一直冒泡到window。
  • 事件冒泡示意图:

    深入理解JavaScript 事件

事件捕获

  • 不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预定目标之前捕获它。IE9+、Safari、Chrome、Opera和Firefox支持,且从window开始捕获(尽管DOM2 级事件规范要求从document)。
  • 事件捕获示意图:

    深入理解JavaScript 事件

  • 由于老版本的浏览器不支持,因此很少有人使用事件捕获。我们也建议读者放心地使用事件冒泡,在有特殊需要时再使用事件捕获
  • 为了彻底理解事件冒泡和捕获,这里写了个例子:

    <!DOCTYPE html>
    <html lang="zh-cn">
    <head>
    <meta charset="UTF-8">
    <title>test1</title>
    <link rel="stylesheet" href="test1.css">
    </head>
    <body>
    <div id="a">
    <div id="b">
    <div id="c"></div>
    </div>
    </div>
    <script src="test1.js"></script>
    </body>
    </html>
#a{
width: 300px;
height: 300px;
background: pink;
} #b{
width: 200px;
height: 200px;
background: blue;
} #c{
width: 100px;
height: 100px;
background: yellow;
}
var a = document.getElementById("a"),
b = document.getElementById("b"),
c = document.getElementById("c"); c.addEventListener("click", function(event){
console.log("c1")
// 注意第三个参数没有传进 false , 因为默认传进来的是 false,代表冒泡阶段调用,个人认为处于目标阶段也会调用的
});
c.addEventListener("click", function(event){
console.log("c2");
}, true);
b.addEventListener("click", function(event){
console.log("b");
}, true);
a.addEventListener("click", function(event){
console.log("a1");
}, true);
a.addEventListener("click", function(event){
console.log("a2")
});
a.addEventListener("click", function(event){
console.log("a3");
event.stopImmediatePropagation();
}, true);
a.addEventListener("click", function(event){
console.log("a4");
}, true);
  • 效果如图
    深入理解JavaScript 事件

  • 点击 c 或 b,输出:a1、a3
  • stopImmediatePropagation 包含了 stopPropagation 的功能,即阻止事件传播(捕获或冒泡),但同时也阻止该元素上后来绑定的事件处理程序被调用,所以不输出 a4,因为事件捕获被拦截了,自然不会触发 b、c 上的事件,所以不输出 b、c1、c2,冒泡更谈不上了,所以不输出 a2。有人会觉得上面的表述有一点点问题,为什么捕获被拦截了,c1 就不输出了呢? c1 应该是冒泡阶段被调用的呀,所以应该改为另一个表述:“...冒泡更谈不上,所以不输出 c1、a2”。但另一个表述是错的,下面会分析到。
  • 点击 a,输出 a1、a2、a3
  • 不应该是 a1、a3、a2 吗?a1、a3 可是在捕获阶段被调用的处理程序啊,a2 是在冒泡阶段被调用的啊。这里正是要说明的:虽然这三个事件处理程序注册时指定了true、false,但现在事件流是处于目标阶段,不是冒泡阶段、也不是捕获阶段,事件处理程序被调用的顺序是注册的顺序。不论你指定的是 true or false. 这也解释了上面提到的“另一种表述”为什么是错误的。
  • 更深一步解释是:要区分事件流和事件处理程序,不论事件处理程序存不存在,事件流都会传播。这是一个观察者模式,绑定事件的节点是被观察者、事件处理程序是观察者,事件流是不知道观察者的存在的,所以你点击页面的时候,事件流一定要传播,传播到某一个节点时,节点去通知所有观察者,也就是调用事件处理程序(有可能观察者不存在)。
  • 当一个事件流来到一个节点时,事件流可能在捕获阶段(正在流向最深层次的节点)、可能在处于目标阶段(已经流到了目标,也就是event.target)、也可能在冒泡阶段(正在流向最外层节点)。而事件处理程序是这么处理的:① 注册时第三个参数指定为 true 时,事件流到来,如果事件流是捕获阶段或处于目标阶段,则调用该事件处理程序。②注册时第三个参数指定为 false 时,当事件流到来,如果事件流是处于目标阶段或冒泡阶段,则调用该事件处理程序。
  • 所以当事件流是处于目标阶段,那么不管事件处理程序第三个参数指定的true or false,事件处理程序都会被调用,调用顺序按照注册顺序。所以点击a,输出 a1、a2、a3,而不是a1、a3、a2。
  • 注释掉 event.stopImmediatePropagation,点击 c,输出 a1、a3、a4、b、c1、c2、a2

  • 另外,如果同一个事件处理程序(指针相同,比如用 handler 保存的事件处理程序),用 addEventListener 或 attachEvent 绑定多次,如果第三个参数是相同的话,也只会被调用一次。但如果第三个参数一个设置为true,另一个设置为false,那么会被调用两次。

DOM事件流

  • “DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段。(事件处理中“处于目标阶段”被看成冒泡阶段的一部分)。
  • IE9、Safari、Chrome、Firefox和Opera9.5及更高版本都会在捕获阶段触发事件对象上的事件,就是有两个机会在目标对象上面操作事件。(尽管DOM2级事件规范明确要求捕获阶段不涉及事件目标)。

事件处理程序

HTML 事件处理程序

简单来讲,HTML 事件处理程序是直接在HTML中绑定事件,如下

<input type="button" value="Click Me" onclick="alert(&quot;Clicked&quot;)" />

注意事项:

  • 不能在其中使用未经转义的HTML语法字符,如&“”<>,因为这是在HTML中绑定的,会造成浏览器解析DOM结构错误。
  • 扩展函数作用域,来看下面的代码:

    <!-- 输出 "Click Me、lzh" -->
    <form method="post">
    <input type="text" name="username" value="lzh">
    <input type="button" value="Click Me" onclick="alert(value);alert(username.value);">
    </form>

    如果当前元素是一个表单输入元素,浏览器内部大概是这样实现的:

    function () {
    with (document) {
    with (this.form) {
    with (this) {
    //元素属性值
    }
    }
    }
    }

    如果没有form元素,调用username会报错,所以不论是服务端渲染还是Ajax请求回来数据再渲染,最好还是把form结构写完整。
    扩展作用域有三个缺点:

  1. 函数被调用时还没定义会报错,只好try{}catch(ex){},分离的写法可以在DOMContentLoaded之后再绑定。
  2. 扩展的作用域链在不同浏览器中会导致不同的结果。
  3. HTML 与 JavaScript 代码紧密耦合,如果要更换事件处理程序,需要改动 HTML 代码和 JavaScript代码。

DOM0级事件处理程序

  • 每个元素(包括window 和document)都有自己的事件处理程序属性,这些属性通常全部小写。使用 DOM0 级指定的事件处理程序被认为是元素的方法。this 引用当前元素。通过 this 可以访问元素的任何属性和方法。DOM0 级事件处理程序在冒泡阶段被处理。
var btn = document.getElementById("myBtn");
btn.onclick = function () {
alert(this.id); //"myBtn"
};

DOM2级事件处理程序

  • addEventListener() 包含三个参数,要处理的事件名、事件处理函数、布尔值,布尔值为true,表示在捕获阶段调用事件处理程序,反之在冒泡阶段调用。
  • DOM2 级事件处理程序中的 this 也指向 addEventListener 的那个元素。
  • 可以添加多个事件处理程序,按添加顺序依次调用。
  • removeEventListener 无法移除匿名函数的事件处理程序。
var btn = document.getElementById("myBtn");
var handler = function () {
alert(this.id);
};
btn.addEventListener("click", handler, false);
//这里省略了其他代码
btn.removeEventListener("click", handler, false); // 有效!
  • IE9、Firefox、Safari、Chrome 和Opera 支持DOM2 级事件处理程序。

IE事件处理程序

  • attachEvent detachEvent 接收两个参数,事件处理程序名称、事件处理程序函数。由于IE8及更早版本只支持事件冒泡,所以该事件处理程序只支持事件冒泡。
  • 老版本的Opera支持这种方法,但现在Opera已经改用blink内核,IE11已经不支持这种方法,注意 IE9 就已经支持 DOM2 级事件处理程序了。
  • 特别要注意:第一个参数包含on,比如onclick。
  • 区别于DOM0 级事件处理程序,this 指向 'window'。
  • 也可以添加多个事件处理程序。

跨浏览器的事件处理程序

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;
}
}
};
  • 存在问题:
  1. IE事件处理程序 中的 this 指向 window
  2. 只支持 DOM0 级的浏览器不能多次添加事件处理程序,不过这种浏览器应该不多了,即使是IE8 也支持attachEvent。
  3. 会不会有一些事件,在浏览器支持 DOM2 级事件处理程序的情况下,那些事件只能用 on + name 的形式呢? 之前一直怀疑 (1).xhr.onreadystatechange() 和 (2).DOMNodeInserted 事件,这里我多虑了,经过验证,(1).是支持 DOM2 级事件的,(2).天生就是 DOM2 级的。这里只是为了打消我的疑虑,记录下来。

事件对象

DOM 中的事件对象

  • 兼容 DOM 的浏览器会将一个 event 对象传入事件处理程序, IE9 及更高版本可以。无论指定事件处理程序时使用什么方法(DOM0 级 DOM2 级),HTML 事件处理程序可以通过访问 event 变量得到 event 对象。
  • event 中的属性和方法都是只读的
  • 常用属性:
  1. target 事件的目标
  2. currentTarget 绑定事件的元素,与 'this' 的指向相同
  3. stopPropagation() 取消事件的进一步捕获或冒泡。如果bubbles为true,则可以使用这个方法
  4. stopImmediatePropagation() 取消事件的进一步捕获或冒泡,同时阻止任何事件处理程序被调用(DOM3级事件中新增)
  5. preventDefault() 取消事件的默认行为,比如点击链接跳转。如果 cancelable 是 true,则可以使用这个方法
  6. type 被触发的事件类型
  7. eventPhase 调用事件处理程序的阶段:1表示捕获阶段,2表示“处于目标”,3表示冒泡阶段
  • this target currentTarget 举例:
document.body.onclick = function(event){
alert(event.currentTarget === document.body); //true
alert(this === document.body); //true
alert(event.target === document.getElementById("myBtn")); //true
};
  • 通过 event.type 与 switch case 组合,可以通过一个函数处理多个事件。
  • 只有在事件处理程序执行期间,event 对象才会存在;一旦事件处理程序执行完成,event 对象就会被销毁。

IE 中的事件对象

  • DOM0 级的事件处理程序,event 作为 window 的一个属性存在。(从 IE9 开始,event 可以从参数中获得)
  • attachEvent 添加的事件处理程序,event 作为参数传入,也可以通过 window 来访问 event 对象。
  • HTML 事件处理程序依然可以通过访问 event 变量得到 event 对象。
  • 属性和方法:
  1. cancelBubble 设置 true or false 可以取消事件冒泡
  2. returnValue 设置 true or false 可以取消事件的默认行为。
  3. srcElement 事件的目标(与DOM中的 target 相同)
  • 注意事项:
  1. attachEvent 中的 event.srcElement === this 吗? 答案是否定的,因为前面说到过 attachEvent 中 this 指向 window, DOM0 级、DOM2 级 事件处理程序 this 才指向 event.target / window.event.srcElement

跨浏览器的事件对象

var EventUtil = {
getEvent: function(event){
return event ? event : window.event; // window.event DOM0级时IE
},
getTarget: function(event){
return event.target || event.srcElement; // event.srcElement for IE
},
preventDefault: function(event){
if (event.preventDefault){
event.preventDefault();
} else {
event.returnValue = false; // IE
}
},
stopPropagation: function(event){
if (event.stopPropagation){
event.stopPropagation();
} else {
event.cancelBubble = true; // IE
}
}
};

事件类型

  • DOM3 级事件规定了几类事件;HTML5 也定义了一组事件;还有一些事件没有规范,浏览器的实现不一致。
  • DOM3 级事件模块在 DOM2 级事件模块基础上重新定义了这些事件,也添加了一些新事件。包括 IE9 在内的所有主流浏览器都支持 DOM2 级事件。IE9 也支持 DOM3 级事件。

这里只总结一些常见的事件类型

UI事件类型

  • load 事件,当页面完全加载后(包括所有图像、JavaScript 文件、CSS 文件等外部资源),就会触发 window 上面的 load 事件。
EventUtil.addHandler(window, "load", function(){
var image = document.createElement("img");
EventUtil.addHandler(image, "load", function(event){
event = EventUtil.getEvent(event);
alert(EventUtil.getTarget(event).src);
});
document.body.appendChild(image);
image.src = "smile.gif"; //在此之前要先指定事件处理程序
});
  1. script 元素也会触发 load 事件,据此可以判断动态加载的 JavaScript 文件是否加载完毕。与图像不同,只有在设置了 script 元素的 src 属性并将该元素添加到文档后,才会开始下载 JavaScript 文件
  2. IE8 及更早版本不支持 script 元素上的 load 事件。
  3. 在不属于 DOM 文档的图像(包括未添加到文档的 img 元素和 Image 对象)上触发 load 事件时,IE8 及之前版本不会生成 event 对象。IE9 修复了这个问题。
  • resize 事件
  1. 浏览器窗口大小发生变化时会触发该事件,这个事件在 window 上触发,IE、Safari、Chrome 和 Opera 会在浏览器窗口变化了 1 像素时就触发 resize 事件,然后随着变化不断重复触发。Firefox 则只会在用户停止调整窗口大小时才会触发。
  2. 注意不要在这个事件的处理程序中加入大计算量的代码,或者采用函数节流的方式优化性能。
  3. 浏览器窗口最小化或最大化时也会触发 resize 事件。
  • scroll 事件
  1. 该事件在 window 上发生,此处和书上讲的有点不一样,webkit 内核或 blink 内核的浏览器(Chrome、Opera、Safari)可以通过 document.body.scrollTop 获取页面被卷去的高度,而 Trident、Gecko (IE、火狐)可以通过 document.documentElement.scrollTop来获取该值。
  2. 另外标准模式、混杂模式这两种方法还有出入,此处不讨论。
  3. 所以最好通过 document.body.scrollTop + document.documentElement.scrollTop 的方式获取 scrollTop 的值,因为两者之一会等于0,或者使用 document.body.scrollTop || document.documentElement.scrollTop,两者效果一致。

焦点事件

  1. 这里忽略 DOMFocusIn、DOMFocusOut,因为只有 Opera 支持这个事件,且 DOM3 级事件废弃了它们。
  2. blur:在元素失去焦点时触发。这个事件不会冒泡;所有浏览器都支持它。
  3. focus:在元素获得焦点时触发。这个事件不会冒泡;所有浏览器都支持它。
  4. focusin:与 focus 等价,但它冒泡。
  5. focusout:与 blur 等价,也冒泡。
  6. 支持 focusin、focusout 的浏览器有:IE5.5+、Safari 5.1+、Opera 11.5+和Chrome。但只支持 DOM2 级事件处理程序
  7. Firefox 不支持 focusin、focusout
  8. blur、focusout 的事件目标是失去焦点的元素;focus、focusin 的事件目标是获得焦点的元素

鼠标与滚轮事件

  • click 在用户单击住鼠标按钮或按下回车键时触发。 触发顺序 mousedown mouseup click,如果 mousedown、mouseup 其中之一被取消,就不会触发 click 事件。
  • dblclick 触发顺序 mousedown mouseup click mousedown mouseup click dblclick, 如果中间有事件被取消,dblclick 也不会被触发
  • mousedown 用户按下了任意鼠标按钮时触发。
  • mouseup 用户释放按钮时触发
  • mouseenter 在鼠标光标从元素外部首次移动到元素范围之内时触发。不冒泡,而且在光标移动到后代元素上不会触发。DOM2 级事件并没有定义这个事,但 DOM3 级事件将它纳入了规范。IE、Firefox9+和Opera支持这个事件。
  • mouseleave 在位于元素上方的鼠标光标移动到元素范围之外时触发。不冒泡,而且在光标移动到后代元素上不会触发。DOM2 级事件并没有定义这个事,但 DOM3 级事件将它纳入了规范。IE、Firefox9+ 和 Opera 支持这个事件。
  • mouseover 在鼠标指针位于一个元素外部,然后用户将其首次移入另一个元素边界之内时触发。不能通过键盘触发这个事件。
  • mouseout 在鼠标指针位于一个元素上方,然后用户将其移入另一个元素时触发。又移入的另一个元素可能位于前一个元素的外部,也可能是这个元素的子元素。不能通过键盘触发这个事件。
  • 用代码说明一下 mouseenter、mouseleave 和 mouseover、mouseout 的区别:
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<title>test1</title>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="test1.css">
</head>
<body>
<div class="mouseover">
<div class="sub-mouseover"> </div>
</div>
<div class="mouseenter">
<div class="sub-mouseenter"> </div>
</div>
<script src="test1.js"></script>
</body>
</html>
.wrap {
width: 200px;
height: 100px;
} .mouseover {
background: pink;
} .mouseenter {
margin-top: 30px;
background: gray;
} .sub-mouseover,
.sub-mouseenter {
width: 100px;
height: 50px;
background: #AE81FF;
}
var div1 = document.querySelector(".mouseover"),
div2 = document.querySelector(".mouseenter"); div1.addEventListener("mouseover", function(){
console.log("div1 mouseover");
});
div1.addEventListener("mouseout", function(){
console.log("div1 mouseout");
}) div2.addEventListener("mouseenter", function(){
console.log("div2 mouseenter");
})
div2.addEventListener("mouseleave", function(){
console.log("div2 mouseleave");
})
  • 效果图
    深入理解JavaScript 事件

  • 鼠标由左侧从上到下依次经过所有 div 的情况,输出 div1 mouseover div1 mouseout div1 mouseover div1 mouseout div2 mouseenter div2 mouseleave

  • mousemove 当鼠标指针在元素内部移动时重复地触发。不能通过键盘触发这个事件。
  • 除了 mouseenter、mousedleave,所有鼠标事件都会冒泡,取消鼠标事件将会影响浏览器的默认行为,也会影响其它事件,因为鼠标事件与其它事件是密不可分的。
  • 关于 dblclick IE8 及之前版本中的实现有一个小bug,因此在双击事件中,会跳过第二个mousedown 和click事件,其顺序如下:mousedown mouseup click mouseup dblclick,但还是会触发 dblclick 事件
  • 客户区坐标位置:鼠标事件中的 event 都有 clientX clientY 属性,表示在视口中客户区的坐标位置,这些值不包括页面滚动的距离,因此这个位置并不表示鼠标在页面上的位置:
    深入理解JavaScript 事件
  • 页面坐标位置:pageX、pageY,这两个属性表示鼠标光标在页面中的位置,在页面没有滚动的情况下,pageX 和 pageY 的值与 clientX、clientY 的值相等。IE8 及更早版本不支持事件对象上的页面坐标,不过使用客户区坐标和滚动信息可以计算出来。这时候需要用到document.body(混杂模式)或document.documentElement(标准模式)中的scrollLeft 和scrollTop 属性。计算过程如下所示:
var div = document.getElementById("myDiv");
EventUtil.addHandler(div, "click", function(event){
event = EventUtil.getEvent(event);
var pageX = event.pageX,
pageY = event.pageY;
if (pageX === undefined){
pageX = event.clientX + (document.body.scrollLeft ||
document.documentElement.scrollLeft);
}
if (pageY === undefined){
pageY = event.clientY + (document.body.scrollTop ||
document.documentElement.scrollTop);
}
alert("Page coordinates: " + pageX + "," + pageY);
});
  • 屏幕坐标位置:screenX、screenY
  • 修改键 用户按住Shift、Ctrl、Alt、Meta(Windows或Cmd,cmd(mac))时触发鼠标事件,可以在 event 中获得修改键。
var div = document.getElementById("myDiv");
EventUtil.addHandler(div, "click", function(event){
event = EventUtil.getEvent(event);
var keys = new Array();
if (event.shiftKey){
keys.push("shift");
}
if (event.ctrlKey){
keys.push("ctrl");
}
if (event.altKey){
keys.push("alt");
}
if (event.metaKey){
keys.push("meta");
}
alert("Keys: " + keys.join(","));
});
  • IE9、Firefox、Safari、Chrome 和Opera 都支持这4 个键。IE8 及之前版本不支持metaKey 属性。另外,旧版本的 IE 有自己的一套写法。
  • 相关元素 mouseover mouseout 时的 event.relatedTarget,不做详细记录。
  • 鼠标按钮 mousedown mouseup 是在按下/释放任意鼠标按钮时触发的,所以通过 event.button: 0(左) 1(中) 2(右) 可以判断按的是哪个键,但是IE8 及更低版本的浏览器不支持,有兼容写法,此处不详细叙述。EventUtil.getButton 有详细实现。
  • mousewheel event.whellDelta 为正数时,向前滚动(回到顶部、页面向下滑动),负数则反过来,这个值是120的倍数,Opera低版本中正负相反,火狐中有自己的一套方法,这里不做详细记录。
  • 触摸设备
  • 不支持dblclick 事件。双击浏览器窗口会放大画面,而且没有办法改变该行为。
  • 轻击可单击元素会触发mousemove 事件。如果此操作会导致内容变化,将不再有其他事件发生;如果屏幕没有因此变化,那么会依次发生mousedown、mouseup 和click 事件。轻击不可单击的元素不会触发任何事件。可单击的元素是指那些单击可产生默认操作的元素(如链接),或者那些已经被指定了onclick 事件处理程序的元素。
  • mousemove 事件也会触发mouseover 和mouseout 事件。
  • 两个手指放在屏幕上且页面随手指移动而滚动时会触发mousewheel 和scroll 事件。
  • 无障碍性问题
  • 如果需要考虑这个问题,不建议使用 click 之外的鼠标事件。因为这个不能通过键盘触发,不利于屏幕阅读器访问。此处不详细记录。

键盘与文本事件

  • keydown: 当用户按下键盘上的任意键时触发,而且如果按住不放的话,会重复触发此事件。
  • keypress 当用户按下键盘上的字符键时触发,而且如果按住不放的话,会重复触发此事件。按下Esc 键也会触发这个事件。Safari 3.1 之前的版本也会在用户按下非字符键时触发keypress事件。
  • keyup:当用户释放键盘上的键时触发。
  • 触发顺序:keydownkeypresskeyupkeydownkeypress 都是在文本框发生变化之前被触发的; keyup 事件则是在文本框已经发生变化之后被触发的。
  • 如果用户按下了一个字符键不放,就会重复触发 keydown 和keypress 事件,直到用户松开该键为止。
  • 键盘事件也支持修改键(ctrl等)
  • keydown、keyup 中的 event 有 keyCode, 与ASCII 码中对应小写字母或数字的编码相同。
  • keypress 中的 event 有 charCode,这个值是按下的那个键所代表字符的 ASCII 编码,用 String.fromCharCode() 可以转换成实际的字符
  • DOM3 级中,有 key 和 char,其中 key 可以直接得到 "k"、"K"、"Shift" 等, char 属性在按下字符键时行为与 key 相同,在按下非字符键时为 null,但是支持还不完整,chrome 总是输出 undefined。
  • keyIdentifier Chrome 已经不推荐使用
  • 表示按下的按键在键盘的位置,比如按下左右侧的shift键,这个值就不同,Chrome 和 Safari 的实现有 bug。
  • textInput: 在文本插入文本框之前会触发textInput 事件。目的是代替keypress,退格键不会触发textInput,但是会触发keypress(只要改变文本),只有真正可以编辑的区域才会触发textInput,但是keypress获得焦点即可触发。event.data中包含用户的输入,拼音输入法中输入过程的拼音不会触发该事件。
  • inputMethod 代表用户是怎样输入的,比如通过粘贴的方式,但是支持的浏览器很少。

变动事件

DOM2 级的变动(mutation)事件能在 DOM 中的某一部分发生变化时给出提示,比如 DOM 节点的插入、移除、特性被修改等等

HTML5 事件

  1. contextmenu 事件
EventUtil.addHandler(window, "load", function(event){
var div = document.getElementById("myDiv"); EventUtil.addHandler(div, "contextmenu", function(event){
event = EventUtil.getEvent(event);
EventUtil.preventDefault(event); var menu = document.getElementById("myMenu");
menu.style.left = event.clientX + "px";
menu.style.top = event.clientY + "px";
menu.style.visibility = "visible";
}); EventUtil.addHandler(document, "click", function(event){
document.getElementById("myMenu").style.visibility = "hidden";
});
});
  1. beforeunload 事件,用户关闭标签页时提示
EventUtil.addHandler(window, "beforeunload", function(event){
event = EventUtil.getEvent(event);
var message = "I'm really going to miss you if you go.";
event.returnValue = message;
return message;
});
  1. DOMContentLoaded 在形成完整DOM树之后就会触发,不理会图像、JavaScript 文件、CSS 文件或其它资源是否已经下载完毕。其实更应该使用 DOMContentLoaded 而不是 window.onload:
EventUtil.addHandler(window, "DOMContentLoaded", function(event){
alert("Content loaded.");
});
EventUtil.addHandler(window, "load", function(event){
alert("Window loaded.");
});
  • IE9+、Firefox、Chrome、Safari 3.1+ 和 Opera9+ 都支持 DOMContentLoaded 事件。
  1. readystatechange 事件,略。
  2. pageshow 和 pagehide 事件,此处要了解 Firefox 和 Opera 有一个特性叫 “往返缓存”(back-forward cache/bfcache),用户点击“前进”、“后退”按钮时,会将页面缓存在内存。不重新加载,JavaScript的状态会保留。但是无论页面是否来自 bfcache,都会触发 pageshow 事件,pageshow 的事件处理程序的 event 对象中有 event.persisted 属性,为 true 代表页面来自bfcache,同样 pagehide 事件触发时,如果页面被保存到 bfcache 中,则该属性为 true。支持pageshow、pagehide 事件的浏览器有 Firefox、Safari5+、Chrome 和 Opera。 IE9 及以前的版本不支持这两个事件。指定了 onunload 事件处理程序的页面会被自动排除在 bfcache 之外。
  3. hashchange 事件。在 window 上触发,event 包含 oldURL、newURL 两个属性。支持该事件的有 IE8+、Firefox3.6+、Safari5+、Chrome 和 Opera10.6+,但oldURL、newURL只有Firefox6+、Chrome和Opera支持。所以最好用 location 来指定当前的 hash:
EventUtil.addHandler(window, "hashchange", function(event){
console.log(location.hash);
})

设备事件

  • orientationchange 事件,屏幕转动。

触摸与手势事件

  • touchstart: 当手指触摸屏幕时触发;即使已经有一个手指放在了屏幕上也会触发。
  • touchmove: 当手指在屏幕上滑动时连续地触发。在这个事件发生期间,调用preventDefault() 可以阻止滚动。
  • touchend:当手指从屏幕上移开时触发。
  • touchcancel:当系统停止跟踪触摸时触发。关于此事件的确切触发时间,文档中没有明确说明。
  • event 对象中包含的常见 DOM 属性有:bubbles、cancelable、view、clientX、clientY、screenX、screenY、detail、altKey、shiftKey、ctrlKey 和metaKey。
  • event 对象中还包含以下用于跟踪触摸的属性:
  1. touches:表示当前跟踪的触摸操作的Touch 对象的数组。
  2. targetTouchs:特定于事件目标的Touch 对象的数组。
  3. changeTouches:表示自上次触摸以来发生了什么改变的Touch 对象的数组。每个Touch 对象包含下列属性:clientX、clientY、pageX、pageY、screenX、screenY、target、identifier(标识触摸的唯一ID)
function handleTouchEvent(event) {

    //only for one touch
if (event.touches.length == 1) { var output = document.getElementById("output");
switch (event.type) {
case "touchstart":
output.innerHTML = "Touch started (" + event.touches[0].clientX + "," + event.touches[0].clientY + ")";
break;
case "touchend":
output.innerHTML += "<br>Touch ended (" + event.changedTouches[0].clientX + "," + event.changedTouches[0].clientY + ")";
break;
case "touchmove":
event.preventDefault(); //prevent scrolling
output.innerHTML += "<br>Touch moved (" + event.changedTouches[0].clientX + "," + event.changedTouches[0].clientY + ")";
break;
}
}
}
  • 一次触摸的事件触发顺序为:touchstart、mouseover、mousemove(一次)、mousedown、mouseup、click、touchend
  • 手势事件:
  1. gesturestart:当一个手指已经按在屏幕上而另一个手指又触摸屏幕时触发。
  2. gesturechange:当触摸屏幕的任何一个手指的位置发生变化时触发。
  3. gestureend:当任何一个手指从屏幕上面移开时触发。
  • 属性有标准的鼠标事件属性,还有两个:rotation(正值表示顺时针)和scale(从1开始)

内存和性能

  • 每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。
  • 必须事先指定所有事件处理程序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。

事件委托

<body>
<ul id="myLinks">
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>
<script type="text/javascript">
(function(){
var list = document.getElementById("myLinks"); EventUtil.addHandler(list, "click", function(event){
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event); switch(target.id){
case "doSomething":
document.title = "I changed the document's title";
break; case "goSomewhere":
location.href = "http://www.wrox.com";
break; case "sayHi":
alert("hi");
break;
}
}); })();
</script>
</body>
  • 上面的方法只取得了一个 DOM 元素,只添加了一个事件处理程序,占用的内存更少。
  • 如果将事件委托到 document 中,会更有优势:
  1. document 对象很快就可以访问,而且可以在页面生命周期的任何时点上为它添加事件处理程序(无需等待 DOMContentLoaded 或 load 事件)。
  2. 在页面中设置事件处理程序所需的时间少。只添加一个事件处理程序所需的 DOM 引用更少,所花的时间也更少。
  3. 整个页面占用的内存空间更少,能够提升整体性能。
  • 最适合采用事件委托技术的事件包块 clickmousedownmouseupkeydownkeyup 和 keypress

移除事件处理程序

  • 如果你知道某个元素即将被移除,那么最好手工移除事件处理程序,因为有的浏览器(尤其是 IE)不会作出恰当地处理,它们很有可能会将对元素和对事件处理程序的引用都保存在内存中。
  • IE8 及更早的版本在页面被卸载(刷新,切换页面)之前没有清理干净事件处理程序,它们会滞留在内存中,可以通过 onunload 事件处理程序移除所有事件处理程序。

模拟事件

  • 在测试 Web 应用程序,模拟触发事件是一种极其有用的技术。DOM2 级规范为此规定了模拟特定事件的方式,IE9、Opera、Firefox、Chrome 和 Safari 都支持这种方式。IE有它自己模拟事件的方式(IE8 及以下才要用到)

DOM 中的事件模拟

  • 可以在 document 对象上使用 createEvent 方法创建 event 对象。这个方法接收一个参数,即表示要创建的事件类型的字符串。在 DOM2 级中,所有这些字符串都使用英文复数形式,而在 DOM3 级中变成了单数。这个字符串可以是下列几个字符串之一:
  1. UIEvents,DOM3 级中是 UIEvent
  2. MouseEvents: 一般化的鼠标事件,DOM3 级中是 MouseEvent
  3. MutationEvents: 一般化的 DOM 变动事件。 ...
  4. HTMLEvents 一般化的 HTML 事件。没有对应的 DOM3 级事件(HTML 事件被分割到其他类别中)

模拟鼠标事件

  • createEvent 方法返回的 event 对象中,有 initMouseEvent() 方法,需要传 15 个参数。type(比如"click"),bubbles(Boolean) 是否冒泡,应该设置为 true, cancelable(Boolean) 应该设置为 true,view(几乎总是document.defaultView), detail(通常设置为0), screenX, screenY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button(表示按下了哪个鼠标,默认0), relatedTarget(只有在模拟 mouseover 或 mouseout时使用)
  • 将 event 对象传给 DOM 节点的 dispatchEvent 方法即可触发事件,如下:
<body>
<input type="button" value="Click me" id="myBtn" />
<input type="button" value="Send click to the other button" id="myBtn2" />
<p>This example works in DOM-compliant browsers (not IE).</p>
<script type="text/javascript"> (function(){
var btn = document.getElementById("myBtn");
var btn2 = document.getElementById("myBtn2"); EventUtil.addHandler(btn, "click", function(event){
alert("Clicked!");
alert(event.screenX); //100
}); EventUtil.addHandler(btn2, "click", function(event){
//create event object
var event = document.createEvent("MouseEvents"); //initialize the event object
event.initMouseEvent("click", true, true, document.defaultView, 0, 100, 0, 0, 0, false,
false, false, false, 0, btn2); //fire the event
btn.dispatchEvent(event); }); })();
</script>
</body>

模拟键盘事件

  • "DOM2 级事件"的草案中本来包含了键盘事件,但在定稿前又被删除了;Firefox 根据其草案实现了键盘事件。但跟 "DOM3 级事件"中的键盘事件有很大区别。
  • DOM3 级规定,调用 createEvent() 并传入 "KeyboardEvent" ,返回键盘事件,有 initKeyEvent() 方法。这个方法接收一下参数
  • type, bubbles, cancelable, view, key(按下的键的键码), location(按下了哪里的键,0:主键盘,1:左,2:右,3:数字键盘,4:虚拟键盘,5:手柄), modifiers: 空格分隔的修改键列表,如 "Shift", repeat(在一行中按了这个键多少次)
    DOM3 级不提倡 keypress 事件, 因此只能模拟 keydown keyup

IE 中的事件模拟

第一步:document.createEventObject()
第二步: 通过赋值的方式初始化事件对象,就是 event.screenX = 0 这些
第三步:btn.fireEvent("onclick", event);

关于标准

    • 由于标准在变,现在 DOM3 级事件已经不推荐使用 document.createEvent 的方式,也不推荐通过 event 对象 initKeyEvent或者 initKeybordEvent,书中的跨浏览器代码在狐火中报错了,因为火狐开始支持 DOM3 级事件,标准又在变,现在 DOM3 级标准推荐通过构造函数的方式初始化模拟事件,但这也还是草案。

    • 关于跨浏览器模拟事件,粗略了解一下 jQuery 的做法,使用了很多 hack,让本来不冒泡的 focus、blur 可以做事件委托,里面的内容还是很多,得另外总结一下。
    • 期待标准被普及的一天:

上一篇:C语言中函数的返回值


下一篇:新增的querySelector、querySelectorAll测试