JavaScript事件(一)
一、事件流
1、事件冒泡
IE事件流被称为事件冒泡,这是因为事件被定义为从最具体的元素(文档树中最深的节点)开始触发,然后向上传播至没有那么具体的元素(文档)。
2、事件捕获
事件捕获是从最不具体的节点应该最先收到事件,而最具体的节点最后收到事件。事件捕获实际上是为了在事件达到目标前拦截事件。
3. DOM事件流
DOM2 Events 规定事件流分为三个阶段:事件捕获、到达目标和事件冒泡。事件捕获最先发生,为提前拦截事件提供了可能。然后,实际的目标接收到事件。最后一个阶段是冒泡,最迟要在这个阶段响应事件。
二、事件处理程序
事件以为着用户或浏览器执行的某种动作。比如单击(click)、加载(load)、鼠标悬停(mouseover)。为响应事件而调用的函数被称为事件处理程序(或事件监听器)。事件处理程序的名字多以“on”开头,因此 click 事件的处理程序叫做 onclick,而 load 事件的处理程序叫做 onload。
HTML 事件处理程序
顾名思义就是以 HTML 属性的方式来指定事件处理程序。此时属性的值必须是能够执行的 Javascript 代码。例如:
<input type="button" value="Click me" onclick="console.log('Clicked')"/>
<!-- 输出"Clicked" -->
注意:因为属性的值是 JavaScript代码,所以不能在未经转义的情况下使用 HTML 语法字符,比如 & 、"、<、>。
还可以调用在页面其他地方定义的脚本,比如函数来执行指令。
以在 HTML 中指定事件处理程序有一些特殊的地方。首先会创建一个函数来封装属性的值。这个函数有一个特殊的局部变量 event,其中保存的就是 event 对象。有了这个对象,就不用开发者另外定义其他变量,也不用从包装函数的参数列表中去取了。
在这个函数中,this 值相当于事件的目标元素。
<input type="button" value="Click me" onclick="console.log(this.value)"/>
<!-- 输出"Click me" -->
这个动态创建的包装函数还有一个特别有意思的地方,就是其作用域链被拓展了。在这个函数中,document 和元素自身的成员都可以被当成局部变量来访问。这是通过with来实现的。这里我就不过多介绍了,感兴趣的小伙伴可以去看《Javascript高级程序设计》里介绍。
在HTML 中指定事件处理程序会存在一些问题。第一个问题是时机问题。有可能HTML已经显示在页面上,用户都与其交互了,而事件处理程序的代码还无法执行。为此,大多数 HTML 事件处理程序会封装在 try/catch 块中,以便在这种情况下静默失败。另一个问题是对事件处理程序作用域链的拓展在不同浏览器中可能会导致不同的结果。不同Javascript引擎中标识符解析的规则存在差异,因此访问无限定的对象成员可能导致错误。
使用 HTML 指定事件处理程序的最后一个问题是 HTML 与 Javascript 强耦合。如果需要修改事件处理程序,则必须在两个地方,即 HTML 和 Javascript 中修改代码。这也是很多开发者不使用 HTML 事件处理程序,而使用 JavaScript 指定事件处理程序的主要原因。
DOM0 事件处理程序
在 JavaScript 中指定事件处理程序的传统方式是把一个函数赋值给(DOM元素的)一个事件处理程序属性。要使用 JavaScript 只当事件处理程序,必须先取得要操作对象的引用。
每个元素(包括 window 和 document)都有有通常小写的事件处理程序属性,比如 onclick。只要把这个属性赋值为一个函数即可:
let btn = document.getElementById("myBtn");
btn.onclick = function() {
console.log("Clicked");
};
这里先从文档中取得按钮,然后给它的 onclick 事件处理程序赋值一个函数。注意前面的代码在运行之后才会给事件处理程序赋值。因此如果在页面中上面的代码出现在按钮之后,则有可能出现用户点击按钮没有反应的情况。
像这样使用 DOM0 方式为事件处理程序赋值时,所附函数被视为元素的方法。因此,事件处理程序会在元素的作用域中运行,即 this 元素。
DOM2 事件处理程序
DOM2 Events 为事件处理程序的赋值和移除定义了两个方法:addEventListener() 和 removeEventListener() 。这两个方法暴露在所有DOM节点上,它们接受了3个参数:时间名、事件处理函数和一个布尔值,true 表示在捕获阶段调用事件处理程序,false(默认值)表示在冒泡阶段调用事件处理程序。例:
let btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
console.log(this.id);
}, false);
与 DOM0 方式类似,这个事件处理程序同样在被附加到的元素的作用域中运行。使用 DOM2 方式的主要优势时可以为同一事件添加多个事件处理程序。
通过 addEventListener() 添加的事件处理程序只能用 removeEventListener() 并传入与添加时同样的参数来移除。这意味着使用 addEventListener() 添加的匿名函数无法移除。
大多数情况下,事件处理程序会被添加到事件流的冒泡阶段,主要原因是跨浏览器兼容性好。把事件处理程序注册到捕获阶段通常用于在事件到达其指定目标之前拦截事件。如果不需要拦截,则不要使用事件捕获。
IE 事件处理程序
IE 实现了 DOM 类似的方法, 即 attachEvent() 和 DetachEvent() 。这两个方法接收两个同样的参数:事件处理处理程序的名字和事件处理函数。因为 IE8 及更早版本只支持事件冒泡,所以使用 attachEvent() 添加的事件处理程序会被添加到冒泡阶段。例:
let btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function() {
console.log("Clicked")
})
注意 attachEvent() 的第一个参数是 “onclick” 而不是 DOM 的 addEventListener() 方法的 “click”。
在 IE 中使用 attachEvent() 与使用 DOM0 方式的主要区别是事件处理程序的作用域。使用 DOM0 方式时,事件处理程序中的 this 值等于目标元素。而使用 attachEvent() 时,事件处理程序是在全局作用域中运行的,因此 this 等于 window。
在分别给同意按钮添加不同的事件处理程序时,这里的事件处理程序会以添加它们的顺序反向输出,也就是说后声明的先输出。
detachEvent() 的用法与 removeEventListener() 相同,这里不再赘述。
跨浏览器事件处理程序
为了兼容不同浏览器,可以做以下封装:
var EventHandler = {
addHandler: function(element, type, handle) {
if(element.addEventListener){
element.addEventListener(type, handle, false);
} else if(element.attachEvent){
element.attachEvent("on" + type, handle);
} else {
element["on" + type] = handle;
}
},
removeHandler: function(element, type, handle) {
if(element.removeEventListener){
element.removeEventListener(type, handle, false);
} else if(element.detachEvent){
element.detachEvent("on" + type, handle);
} else {
element["on" + type] = null;
}
}
}