JS與HTML之間的交互作用透過事件實現。事件就是文件或瀏覽器視窗中發生的一些特定的互動瞬間。可以使用監聽器(或處理程序)來預定事件,以便在事件發生時執行對應的程式碼。這種在傳統軟體工程中被稱為觀察員模式,支援頁面的行為與頁面的外觀之間的鬆散耦合。本文將介紹與JS事件相關的基礎知識。
一、事件流事件流描述的是從頁面中接受事件的順序。
事件冒泡事件開始時由最具體的元素(文檔中嵌套層次最深的那個節點)接收,然後逐級向上傳播到較為不具體的結點(文檔)。以下面HTML頁面為例,如果你點選了頁面中的按鈕,那麼」click」事件會依照37a9605a910bf1fbe848fe3f47ab78d9、1d6e7d87652dd104f173dbf7284e2799、07e6e06e0dc95dc83bb70d14dca11cbe、document的順序傳播。換句話說,事件冒泡指的就是事件從底層觸發事件的元素開始沿著DOM樹向上傳播,直到document物件。
<html>
<head>
<title>Test</title>
</head>
<body>
<button id="myBtn">A Btn</button>
</body>
</html>
事件捕獲與事件冒泡的思路相反,事件捕獲的想法是不太具體的節點應該更早地接收到事件,最具體的結點應該最後才接收事件。同樣還是上面那個例子,點擊頁面中的按鈕之後,”click”事件會按照document、07e6e06e0dc95dc83bb70d14dca11cbe、1d6e7d87652dd104f173dbf7284e2799、37a9605a910bf1fbe848fe3f47ab78d9的順序傳播。換句話說,事件捕獲就是指事件從document物件開始沿著DOM樹向下傳播,直到事件的實際目標元素。
DOM事件流「DOM2級事件」規定的事件包括三個階段: 事件擷取階段、處於目標階段和事件冒泡階段。首先發生的是事件捕獲,為截獲事件提供了機會。然後是實際的目標接收到事件。最後一個階段是冒泡階段,可以在這個階段對事件做出反應。
還是以先前的點擊按鈕為例,在DOM事件流中,捕獲階段,」click」事件從document開始向下傳遞到body元素(注意,實際目標button在捕獲階段不會接收到事件)。目標階段,button元素接收到”click”事件。最後,冒泡階段,事件又被傳回文件。
二、事件處理程序事件是使用者或瀏覽器本身執行的某種動作,而回應某個事件的函數就叫做事件處理程序或事件偵聽器。
HTML事件處理程序這裡的HTML事件處理程序指的是直接在HTML元素裡面透過特性(attribute)定義的事件處理程序,請看下面的程式碼範例。這樣是定的事件處理程序會建立一個封裝著元素屬性值的函數,this值等於事件的目標元素。透過這種方法指定事件處理程序有不少缺點,不建議使用。
<button onclick="alert('HaHa~')">Btn-1</button>
<button onclick="alert('event.type')">Btn-2</button>
<button onclick="handler()">Btn-3</button>
<script type="text/javascript">
function handler() {
alert("Haha~");
}
</script>
DOM0級事件處理程序透過JS指定事件處理程序的傳統方式就是將一個函數賦值給一個事件處理程序屬性,請看下面程式碼範例。透過這種方式指定的事件處理程序是在元素的作用域中執行,this引用的是當前元素。這種方式新增的事件處理程序會在事件流的冒泡階段被處理。若要刪除事件,直接令onclick的值為空即可。
var btn = document.getElementById("myBtn");
btn.onclick = function() {
console.log("this.id"); // "myBtn"
};
// 删除事件处理程序
btn.onclick = null;
DOM2級事件處理程序「DOM2級事件」定義了兩個方法用於指定和刪除事件處理程序,addEventListener()和removeEventListener()。所有DOM節點中都包含這兩個方法。這兩個方法都接收3個參數,要處理的事件、處理函數、布林值。最後的布林值為true時表示在捕獲階段呼叫事件處理程序,為false時表示在冒泡階段呼叫處理程序。與DOM0級方法一樣,這裡加入的事件處理程序也是在其依附的元素的作用域中運作。 DOM2級方法加入事件處理程序的優點是可以新增多個事件處理程序。這些事件處理程序會按照它們被新增的順序觸發。以下是程式碼範例:
var btn = document.getElementById("myBtn");
// 添加,触发点击事件时先输出"myBtn"再输出"HaHa~"
btn.addEventListener("click", function() {
console.log(this.id);
}, false);
btn.addEventListener("click", function() {
console.log("HaHa~");
}, false);
透過addEventListener()新增的事件只能透過removeEventListener()來刪除。刪除時傳入的參數與新增時使用的參數應保持一致。這也意味著透過addEventListener()新增的匿名函數將無法刪除,因為無法將新增時傳遞的匿名函數傳給removeEventListener(),即便在刪除的時候寫了一個一模一樣的函數,但此時這個函數只是一個新的匿名函數。請看下面程式碼範例:
var btn = document.getElementById("myBtn");
// 无法删除匿名函数
btn.addEventListener("click", function() {
console.log(this.id);
}, false);
btn.removeEventListener("click", function() {
console.log(this.id);
}, false);
// 正确的添加和删除方式
function handler() {
console.log(this.id);
}
btn.addEventListener("click", handler, false);
btn.removeEventListener("click", handler, false);
大多數情況下,都是將事件處理程序新增至事件流的冒泡階段,這樣可以最大限度地相容於各種瀏覽器。最好只在需要在事件到達目標之前截獲它的時候才將事件處理程序新增至擷取階段。 JS高階程式設計上給出的建議是,如果不是特別需要,不建議在事件擷取階段註冊事件處理程序。
IE事件處理程序#IE实现了与DOM中类似的两个方法: attachEvent()和deleteEvent()。这两个方法接收两个参数,事件处理程序名称和事件处理程序。注意,第一个参数是事件处理程序名称而不是事件名称,也就是说在注册点击事件的处理程序时应该传入”onclick”而不是”click”,这里跟DOM的方法有些差别。另外,这两个方法注册的事件处理程序是在全局作用域中运行而不是元素作用域,this的值指向window。还有一点需要特别小心,通过attachEvent()方法也可以添加多个事件处理程序,但是它们的执行顺序却不是按照它们被添加的顺序,而是完全相反,跟DOM方法截然不同。突然觉得IE真的特别反人类~~~下面是代码示例:
var btn = document.getElementById("myBtn");
function handler1() { // ... }
function handler2() { // ... }
// 添加,触发点击事件时先执行handler2再执行handler1
btn.attachEvent("onclick", handler1);
btn.attachEvent("onclick", handler2);
// 删除
btn.deleteEvent("onclick", handler1);
btn.deleteEvent("onclick", handler2);
三、事件对象在触发DOM上的某个事件时,会产生一个事件对象event,这个对象中包含着所有与事件有关的信息,包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。
DOM中的事件对象兼容DOM的浏览器会将一个event对象传入事件处理程序中,无论指定事件处理程序时用的是DOM0还是DOM2的方法,都会传入event对象。event对象只有在事件处理程序执行期间才会存在,一旦事件处理程序执行完毕,event对象就会被销毁。下面是代码示例:
var btn = document.getElementById("myBtn");
btn.onclick = function(event) {
console.log(event.type); // "click"
}
btn.addEventListener("click", function(event) {
console.log(event.type);
}, false);
event对象包含与创建它的特定事件有关的属性和方法,触发的事件类型不一样,可用的属性方法也有所不同。但是所有的事件都会有下列的属性或方法:
bubbles: 布尔值,表示事件是否冒泡
cancelable: 布尔值,表示是否可以取消事件的默认行为
currentTarget: 元素,事件处理程序当前正在处理事件的那个元素
defaultPrevented: 布尔值,表示是否调用过preventDefault()方法
detail: 整数,与事件相关的细节信息
eventPhase: 整数,调用事件处理程序的阶段,1表示捕获阶段,2表示目标阶段,3表示冒泡阶段
preventDefault(): 函数,取消事件的默认行为,cancelable为true时可以调用该方法
stopImmediatePropagation(): 函数,取消事件的进一步捕获或冒泡,同时阻止任何事件处理程序被调用
stopPropagation(): 函数,取消事件的进一步捕获或冒泡,bubbles为true时可以调用这个方法
target: 元素,事件的目标
trusted: 布尔值,为true时表示事件是浏览器生成的,否则表示事件是通过JS创建的
type: 字符串,被触发的事件类型
view: 与事件关联的抽象视图,等同于发生事件的window对象
下面代码示例展示了上述部分属性的用法,也可以帮助我们进一步理解事件流。假设页面中有一个按钮”myBtn”。当点击按钮时,this和currentTarget都等于body元素,因为事件处理程序是注册在body元素上。target的值却等于按钮元素,因为它是click事件的真正目标。由于按钮上没有注册事件处理程序,结果”click”事件冒泡到了document.body那里才得到处理。
document.body.onclick = function(event) {
console.log(event.currentTarget === document.body); // true
console.log(this === document.body); // true
console.log(event.target === document.getElementById("myBtn")); // true
};
再看一个例子,下面代码中,stopPropagation()方法取消了事件的进一步捕获或冒泡。当我点击按钮时,本来应该会因为事件冒泡机制触发按钮和body元素上的点击事件处理程序,输出”From Bth …”和”From Body …”。现在点击事件在按钮元素上触发之后就被阻止继续在DOM层次中的传播,因此body上的事件处理程序不会被触发。
var btn = document.getElementById("myBtn");
btn.onclick = function(event) {
console.log("From Bth ...");
event.stopPropagation(); // 停止事件传播
};
document.body.onclick = function() {
console.log("From Body ...");
};
IE中的事件对象在IE中,使用DOM0的方法添加事件处理程序时,event对象作为window对象的一个属性存在。如果是通过attachEvent()方法添加,则event对象是作为参数传入事件处理函数。下面是代码示例:
var btn = document.getElementById("myBtn");
btn.onclick = function() {
var event = window.event;
console.log(event.type); // "click"
};
btn.attachEvent("onclick", function(event) {
console.log(event.type); // "click"
});
IE的event对象同样也包含与创建它的事件相关的属性和方法,这些属性和方法也会因为事件类型的不同而有所差异。但所有事件对象都会包含下列属性:
cancelBubble: 布尔值,可读可写,默认为false。将其设置为true时取消事件冒泡
returnValue: 布尔值,可读可写,默认为true。将其设置为false时取消事件的默认行为
srcElment: 元素,事件的目标元素,与DOM中的target属性相同
type: 字符串,事件类型在IE中,事件处理程序的作用域是根据指定它的方式来确定,this的值不一定是指向事件的目标元素。因此,使用srcElement属性更具保险。请看下面代码实例,第一种方式中this的值为目标元素,而第二种方式,前面讲过这种方式的事件处理程序是在全局作用域中执行,因此this的值为window。
var btn = document.getElementById("myBtn");
btn.onclick = function() {
console.log(window.event.srcElement === this); // true
}
btn.attachEvent("onclick", function(event) {
console.log(event.srcElement === this); // false
});