13 事件

weixin_33670713發表於2016-05-09

本章內容

  • 理解事件流
  • 使用事件處理程式
  • 不同的事件型別

JavaScript 與 HTML 之間的互動是通過事件實現的。事件,就是文件或瀏覽器視窗中發生的一些特定的互動瞬間。

13.1 事件流

事件流描述的是從頁面中接收事件的順序。IE 的事件流是事件冒泡流,而 Netscape Communicator 的事件流失事件捕獲流。

13.1.1 事件冒泡

IE 的事件流叫做事件冒泡,即事件開始時由最具體的元素接收,然後逐級向上傳播到較為不具體的節點。以下面的 HTML 頁面為例:

<!DOCTYPE html>
<html>
<head>
  <title>event bubbling example</title>
</head>
<body>
  <div>click me</div>
</body>
</html>

如果單擊了頁面中的<div>元素,那麼這個click事件就會按照如下順序傳播:

  1. <div>
  2. <body>
  3. <html>
  4. document

所有現代瀏覽器都支援事件冒泡,但在具體實現上還是有一些差別。

13.1.2 事件捕獲

事件捕獲的思想是不太具體的節點應該更早接收到事件,而最具體的節點應該最後接收到事件。事件捕獲的用意在於在事件到達預定目標之前捕獲它。前述例子會按下列順序觸發click事件。

  1. document
  2. <html>
  3. <body>
  4. <div>

雖然事件捕獲是 Netscape Communicator 唯一支援的事件流模型,但 其他主流瀏覽器目前也都支援這種事件流模型。
建議放心地使用事件冒泡,在有特殊需要時再使用事件捕獲。

13.1.3 DOM 事件流

“DOM2級事件”規定的事件流包括三個階段:事件捕獲階段、處於目標階段和事件冒泡階段。首先傳送的是事件捕獲,為截獲事件提供了機會。然後是實際的目標接收到事件。最後一個階段是冒泡階段,可以在這個階段對事件做出響應。
在 DOM 事件流中,實際的目標(<div>元素)在捕獲階段不會接收到事件。也就是說,事件從document<html>再到<body>後就停止了。下一個階段是“處於目標”階段,於是事件在<div>上發生,並在事件處理中被看成冒泡階段的一部分。然後,冒泡階段發生,事件又傳播迴文件。
多數支援 DOM 事件流的瀏覽器都實現了一種特定的行為:即使“DOM2 級事件”規範明確要求捕獲階段不會涉及事件目標,但高版本瀏覽器都會在捕獲階段觸發事件物件上的事件。結果,就是有兩個機會再目標物件上面操作事件。

13.2 事件處理程式

事件就是使用者或瀏覽器自身執行的某種動作。諸如clickloadmouseover,都是事件的名字。而響應某個事件的函式就叫做事件處理程式(或事件偵聽器)。事件處理程式的名字以“on”開頭,因此click事件的事件處理程式就是onclickload事件的事件處理程式就是onload。為事件指定處理程式的方式有好幾種。

13.2.1 HTML 事件處理程式

某個元素支援的每種事件,都可以使用一個與相應事件處理程式同名的 HTML 特性來指定。這個特性的值應該是能夠執行的 JavaScript 程式碼。例如:

<input type="button" value="click me" onclick ="alert('clicked')" />

在 HTML 中定義的事件處理程式可以包含要執行的具體動作,也可以呼叫在頁面其他地方定義的指令碼,如下例所示:

<script type="text/javascript">
  function showMessage() {
    alert('hello world!');
  }
</script>
<input type="button" value="click me" onclick="showMessage()" />

事件處理程式中的程式碼在執行時,有權訪問全域性作用域中的任何程式碼。
這樣指定事件處理程式具有一些獨到之處。首先,這樣會建立一個封裝著元素屬性值的函式。這個函式中有一個區域性變數event,也就是事件物件。
不過,在 HTML 中指定事件處理程式有兩個缺點。首先,存在一個時差問題。因為使用者可能會在 HTML 元素一出現在頁面上就觸發相應的事件,但當時的事件處理程式有可能尚不具備執行條件。
另一個缺點是,這樣擴充套件事件處理程式的作用域在不同瀏覽器中會導致不同結果。
最後一個缺點是 HTML 與 JavaScript 程式碼緊密耦合。故推薦使用 JavaScript 指定事件處理程式。

13.2.2 DOM0 級事件處理程式

通過 JavaScript 指定事件處理程式的傳統方式,就是將一個函式賦值給一個事件處理程式屬性。這種為事件處理程式賦值的方法是在第四代 Web 瀏覽器中出現的,而且至今仍然為所有現代瀏覽器所支援。原因一是簡單,二是具有跨瀏覽器的優勢。要使用 JavaScript 指定事件處理程式,首先必須取得一個要操作的物件的引用。
每個元素都有自己的事件處理程式屬性,這些屬性通常全部小寫,例如onclick。將這種屬性的值設定為一個函式,就可以指定事件處理程式,如下所示:

var btn = document.getElementById('myBtn');
btn.onclick = function () {
  alert('clicked');
};

使用 DOM0 級方法指定的事件處理程式被認為是元素的方法。因此,這時候的事件處理程式是在元素的作用域中執行;換句話說,程式中的this引用當前元素。如下例:

var btn = document.getElementById('myBtn');
btn.onclick = function () {
  alert(this.id);  //"myBtn"
};

以這種方式新增的事件處理程式會在事件流的冒泡階段被處理。
可以刪除通過 DOM0 級方法指定的事件處理程式,只要像下面這樣將事件處理程式屬性的值設定為null即可。

btn.onclick = null;  //刪除事件處理程式

13.2.3 DOM2 級事件處理程式

“DOM2級事件”定義了兩個方法,用於處理指定和刪除事件處理程式的操作:addEventListener()removeEventListener()。所有 DOM 節點中都包含這兩個方法,並且它們都接受 3 個引數:要處理的事件名、作為事件處理程式的函式和一個布林值。最後這個布林值引數如果是true,表示在捕獲階段呼叫事件處理程式;如果是false,表示在冒泡階段呼叫事件處理程式。
要在按鈕上為click事件新增事件處理程式,可以使用下列程式碼:

var btn = document.getElementById("myBtn");
btn.addEventListener('click', function () {
  alert(this.id);
}, false);

與 DOM0 級方法一樣,這裡新增的事件處理程式也是在其依附的元素的作用域中執行。使用 DOM2 級方法新增事件處理程式的主要好處是可以新增多個事件處理程式。如下例:

var btn = document.getElementById('myBtn');
btn.addEventListener("click", function () {
  alert(this.id);
}, false);
btn.addEventListener("click", function () {
  alert("Hello world!");
}, false);

這裡為按鈕新增了倆事件處理程式。這兩個事件處理程式會按照新增它們的順序觸發,因此首先會顯示元素的 ID,其次會顯示“Hello world!”訊息。
通過addEventListener()新增的事件處理程式只能使用removeEventListener()來移除;移除時傳入的引數與新增處理程式時使用的引數相同。這也意味著通過addEventListener()新增的匿名函式將無法移除,如下例:

var btn = document.getElementById("myBtn");
btn.addEventListener("click", function () {
  alert(this.id);
}, false);
//這裡省略了其他程式碼
btn.removeEventListener("click", function() {
  alert(this.id);  //沒有用
}, false)

再看看下例:

var btn = document.getElementById("myBtn");
var handler = function () {
  alert(this.id);
};
btn.addEventListener("click", handler, false);
//這裡省略了其他程式碼
btn.removeEventListener("click", handler, false);  //有效!

大多數情況下,都是將事件處理程式新增到事件流的冒泡階段,這樣可以最大限度地相容各種瀏覽器。

13.2.4 IE 事件處理程式

IE 實現了與 DOM 中類似的兩個方法:attachEvent()detachEvent()。這兩個方法接受相同的兩個引數:事件處理程式名稱與事件處理程式函式。由於 IE8 及更早版本只支援事件冒泡,所以通過attachEvent()新增的事件處理程式都會被新增到冒泡階段。
要使用attachEvent()為按鈕新增一個事件處理程式,可以使用以下程式碼。

var btn = document.getElementById('myBtn');
btn.attachEvent('onclick', function() {
  alert("clicked");
});

注意,attachEvent()的第一個引數是"onclick"
在 IE 中使用attachEvent()與使用 DOM0 級方法的主要區別在於事件處理程式的作用域。在使用 DOM0 級方法的情況下,事件處理程式會在其所屬元素的作用域內執行;在使用attachEvent()方法的情況下,事件,事件處理程式會在全域性作用域中執行,因此this等於window。看下例:

var btn = document.getElementById('myBtn');
btn.attachEvent("onclick", function () {
  alert(this === window);  //true
});
var btn = document.getElementById('myBtn');
btn.attachEvent("onclick", function () {
  alert("clicked");
});
btn.attachEvent("onclick", function () {
  alert("Hello world!");
});

與 DOM 方法不同的是,這些事件處理程式不是以新增它們的順序執行,而是以相反的順序被觸發。
使用attachEvent()新增的事件可以通過detachEvent()來移除。

var btn  = document.getElementById("myBtn");
var handler = function () {
  alert("clicked");
};
btn.attachEvent("onclick", handler);
//這裡省略了其他程式碼
btn.detachEvent("onclick", handler);

13.2.5 跨瀏覽器的事件處理程式

為了以跨瀏覽器的方式處理事件,不少開發人員會使用能夠隔離瀏覽器差異的 JavaScript 庫,還有一些會自己開發最合適的事件處理的方法。自己編寫程式碼其實也不難,只要恰當地使用能力檢測即可。要保證處理事件的程式碼能在大多數瀏覽器下一致地執行,只需關注冒泡階段。
第一個要建立的方法是addHandler(),它的職責是視情況分別使用 DOM0 級方法、DOM2 級方法或 IE 方法來新增事件。這個方法屬於一個名叫EventUtil的物件,本書將使用這個物件來處理瀏覽器間的差異。addHandler()方法接受 3 個引數:要操作的元素、事件名稱和事件處理程式。
addHandler()對應的方法是removeHandler(),它也接受相同的引數。這個方法的職責是移除之前新增的事件處理程式,預設採用 DOM0 級方法。

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;
  }
} 

可以像下面這樣使用EventUtil物件:

var btn = document.getElementById("myBtn");
var handler = function () {
  alert("clicked");
};
EventUtil.addHandler(btn, "click", handler);
//這裡省略了其他程式碼
EventUtil.removeHandler(btn, "click", handler);

addHandler()removeHandler()沒有考慮到所有的瀏覽器問題,例如在 IE 中的作用域問題。

13.3 事件物件

在觸發 DOM 上的某個事件時,會產生一個事件物件event,這個物件中包含著所有與事件有關的資訊。包括導致事件的元素、事件的型別以及其他與特定事件相關的資訊。例如,滑鼠操作導致的事件物件中,會包含滑鼠位置的資訊,而鍵盤操作導致的事件物件中,會包含與按下的鍵有關的資訊。所有瀏覽器都支援event物件,但支援方式不同。

13.3.1 DOM 中的事件物件

相容 DOM 的瀏覽器會將一個event物件傳入到事件處理程式中。

var btn = document.getElementById("myBtn");
btn.onclick = function (event) {
  alert(event.type);  //"click"
};
btn.addEventListener('click', function(event) {
  alert(event.type);  //"click"
}, false);

在事件處理程式內部,物件this始終等於currentTarget的值,而target則只包含事件的實際目標。如果直接將事件處理程式指定給了目標元素,則thiscurrentTargettarget包含相同的值。

var btn = document.getElementById('myBtn');
btn.onclick = function(event) {
  alert(event.currentTarget === this);  //true
  alert(event.target === this);  //true
};

如果事件處理程式存在於按鈕的父節點中,那麼這些值是不相同的。

document.body.onclick = function(event) {
  alert(event.currentTarget === document.body);  //true
  alert(this === document.body);  //true
  alert(event.target === document.getElementById('myBtn'));  //true
}

在需要通過一個函式處理多個事件時,可以使用type屬性。

var btn = document.getElementById('myBtn');
var handler = function(event) {
  switch(event.type) {
    case "click":
      alert('clicked');
      break;
    case "mouseover":
      event.target.style.backgroundColor = "red";
      break;
    case "mouseout":
      event.target.style.backgroundColor = "";
      break;
  }
};
btn.onclick = handler;
btn.onmouseover = handler;
btn.onmouseout = handler;

要阻止特定事件的預設行為,可以使用preventDefault()方法。例如:連結的預設行為就是在被單機時會導航到其href特性指定的 URL。若想要阻止連結導航這一預設行為,那麼通過連結的onclick事件處理程式可以取消它。

var link = document.getElementById('myLink');
link.onclick = function(event) {
  event.preventDefault();
};

只有cancelable屬性設定為true的事件,才可以使用preventDefault()來取消其預設行為。
另外,stopPropagation()方法用於立即停止事件在 DOM 層次中的傳播,即取消進一步的事件捕捉或冒泡。例如:

var btn = document.getElementById('myBtn');
btn.onclick = function(event) {
  alert('Clicked');
  event.stopPropagation();
};
document.body.onclick = function(event) {
  alert('body clicked');
};

事件物件的eventPhase屬性,可以用來確定事件當前正位於事件流的那個階段。如果是在捕獲階段呼叫的事件處理程式,那麼等於1;如果事件處理程式處於目標物件上,則等於2;如果是在冒泡階段呼叫的事件處理程式,則等於3。儘管“處於目標”發生在冒泡階段,但eventPhase仍然一直等於2

var btn = document.getElementById('myBtn');
btn.onclick = function(event) {
  alert(event.eventPhase);  //2
};
document.body.addEventListener('click', function(event) {
  alert(event.eventPhase);  //1
}, true);
document.body.onclick = function(event) {
  alert(event.eventPhase);  //3
};

只有在事件處理程式執行期間,event物件才會存在,一旦事件處理程式執行完成,event物件就會被銷燬。

13.3.2 IE 中的事件物件

要訪問 IE 中的event物件有幾種不同的方式,取決於指定事件處理程式的方法。在使用 DOM0 級方法新增事件處理程式時,event物件作為window物件的一個屬性存在。

var btn = document.getElementById("myBtn");
btn.onclick = function () {
  var event = window.event;
  alert(event.type);  //"click"
};

13.3.3 跨瀏覽器的事件物件

IE 中event物件的全部資訊和方法 DOM 物件中都有,只不過實現方式不一樣。

var EventUtil = {
  addHandler: function(element, type, handler) {
    //省略的程式碼
  },
  getEvent: function(event) {
    return event ? event : window.event;
  },
  getTarget: function(event) {
    return event.target || event.srcElement;
  },
  preventDefault: function (event) {
    if (event.preventDefault) {
      event.preventDefault();
    } else {
      event.returnValue = false;
    }
  },
  removeHandler: function(element, type, handler) {
    //省略的程式碼
  },
  stopPropagation: function(event) {
    if (event.stopPropagation) {
      event.stopPropagation();
    } else {
      event.cancelBubble = true;
    }
  }
};
btn.onclick = function(event) {
  event = EventUtil.getEvent(event);
};

13.4 事件型別

“DOM3級事件”規定了一下幾類事件。

  • UI 事件,當使用者與頁面上的元素互動時觸發;
  • 焦點事件,當元素獲得或失去焦點時觸發;
  • 滑鼠事件,當使用者通過滑鼠在頁面上執行操作時觸發;
  • 滾輪事件,當使用滑鼠滾輪(或類似裝置)時觸發;
  • 文字事件,當在文件中輸入文字時觸發;
  • 鍵盤事件,當使用者通過鍵盤在頁面上執行操作時觸發;
  • 合成事件,當為 IME(Input Method Editor)輸入字元時觸發;
  • 變動事件,當底層 DOM 結構發生變化時觸發。
  • 變動名稱事件,當元素或屬性名變動時觸發。此類事件已經被廢棄,沒有任何瀏覽器實現它們,因此本章不做介紹。

13.4.1 UI 事件

var isSupported = document.implementation.hasFeature("HTMLEvents", "2.0");
var isSupported = document.implementation.hasFeature("UIEvent", "3.0");
  1. load 事件
    當頁面完全載入後(包括所有影象、JavaScript 檔案、CSS 檔案等外部資源),就會觸發window上面的load事件。
    影象上面也可以觸發load事件,無論是在 DOM 中的影象元素還是 HTML 中的影象元素。
  2. unload 事件
    這個事件在文件被完全解除安裝後觸發。只要使用者從一個頁面切換到另一個頁面,就會發生unload事件。而利用這個事件最多的情況是清除引用,以避免記憶體洩漏。
  3. resize 事件
    當瀏覽器視窗被調整到一個新的高度或寬度時,就會觸發resize事件。這個事件在window上面觸發,因此可以通過 JavaScript 或者<body>元素中的onresize特性來指定事件處理程式。
  4. scroll 事件
    雖然scroll事件是在window物件上發生的,但它實際表示的則是頁面中相應元素的變化。

13.4.2 焦點事件

焦點事件會在頁面獲得或失去焦點時觸發。利用這些事件並與document.hasFocus()方法及document.activeElement屬性配合,可以知曉使用者在頁面上的行蹤。

13.4.3 滑鼠與滾輪事件

13.4.4 鍵盤與文字事件

1.鍵碼
2.字元編碼
3.DOM3 級變化
4.textInput事件
5.裝置中的鍵盤事件

13.4.5 複合事件

13.4.6 變動事件

13.4.7 HTML5 事件

  1. contextmenu 事件
  2. beforeunload 事件
  3. DOMContentLoaded 事件
  4. readystatechange 事件
  5. pageshow 和 pagehide 事件
  6. hashchange 事件

13.4.8 裝置事件

13.4.9 觸控與手勢事件

13.5 記憶體和效能

13.5.1 事件委託

對“事件處理程式過多”問題的解決方案就是事件委託。事件委託利用了事件冒泡。

13.5.2 移除事件處理程式

btn.onclick = function() {
  btn.onclick = null;
  document.getElementById("myDiv").innerHTML = "Processing...";
}

13.6 模擬事件

13.7 小結

在使用事件時,需要考慮如下一些記憶體與效能方面的問題。

  • 有必要限制一個頁面中事件處理程式的數量,數量太多會導致佔用大量記憶體,而且也會讓使用者感覺頁面反應不夠靈敏。
  • 建立在事件冒泡機制之上的事件委託技術,可以有效地減少事件處理程式的數量。
  • 建議在瀏覽器解除安裝頁面之前移除頁面中的所有事件處理程式。
    可以使用 JavaScript 在瀏覽器中模擬事件。

相關文章