JavaScript 事件模型

這是我參與8月更文挑戰的第4天,活動詳情查看:8月更文挑戰html

EventTarget 接口

EventTarget接口概述

DOM 的事件操做(監聽和觸發),都定義在EventTarget接口。全部節點對象都部署了這個接口,其餘一些須要事件通訊的瀏覽器內置對象(好比,XMLHttpRequestAudioNodeAudioContext)也部署了這個接口。node

該接口主要提供三個實例方法。web

  • addEventListener:綁定事件的監聽函數
  • removeEventListener:移除事件的監聽函數
  • dispatchEvent:觸發事件

EventTarget.addEventListener()

EventTarget.addEventListener()用於在當前節點或對象上,定義一個特定事件的監聽函數。一旦這個事件發生,就會執行監聽函數。該方法沒有返回值。編程

target.addEventListener(type, listener[, useCapture]);
複製代碼

該方法接受三個參數。瀏覽器

  • type:事件名稱,大小寫敏感。
  • listener:監聽函數。事件發生時,會調用該監聽函數。
  • useCapture:布爾值,表示監聽函數是否在捕獲階段(capture)觸發,默認爲false(監聽函數只在冒泡階段被觸發)。該參數可選。

下面是一個例子。markdown

function hello() {
  console.log('Hello world');
}

var button = document.getElementById('btn');
button.addEventListener('click', hello, false);
複製代碼

上面代碼中,button節點的addEventListener方法綁定click事件的監聽函數hello,該函數只在冒泡階段觸發。函數

上面代碼中,addEventListener方法的第二個參數,就是一個具備handleEvent方法的對象。post

其次,第三個參數除了布爾值useCapture,還能夠是一個屬性配置對象。該對象有如下屬性。ui

  • capture:布爾值,表示該事件是否在捕獲階段觸發監聽函數。
  • once:布爾值,表示監聽函數是否只觸發一次,而後就自動移除。
  • passive:布爾值,表示監聽函數不會調用事件的preventDefault方法。若是監聽函數調用了,瀏覽器將忽略這個要求,並在監控臺輸出一行警告。

若是但願事件監聽函數只執行一次,能夠打開屬性配置對象的once屬性。this

element.addEventListener('click', function (event) {
   // 只執行一次的代碼 且 this 指向 element
    console.log(this.nodeName); // "element"

}, {once: true});
複製代碼

addEventListener方法能夠爲針對當前對象的同一個事件,添加多個不一樣的監聽函數。這些函數按照添加順序觸發,即先添加先觸發。若是爲同一個事件屢次添加同一個監聽函數,該函數只會執行一次,多餘的添加將自動被去除(沒必要使用removeEventListener方法手動去除)。

上面代碼中,監聽函數內部的this指向事件所在的對象element

EventTarget.removeEventListener()

EventTarget.removeEventListener方法用來移除addEventListener方法添加的事件監聽函數。該方法沒有返回值。

div.addEventListener('click', listener, false);
div.removeEventListener('click', listener, false);
複製代碼

removeEventListener方法的參數,與addEventListener方法徹底一致。它的第一個參數「事件類型」,大小寫敏感。

注意,removeEventListener方法移除的監聽函數,必須是addEventListener方法添加的那個監聽函數,並且必須在同一個元素節點,不然無效。

EventTarget.dispatchEvent()

EventTarget.dispatchEvent方法在當前節點上觸發指定事件,從而觸發監聽函數的執行。該方法返回一個布爾值,只要有一個監聽函數調用了Event.preventDefault(),則返回值爲false,不然爲true

target.dispatchEvent(event)
複製代碼

dispatchEvent方法的參數是一個Event對象的實例。

para.addEventListener('click', hello, false);
var event = new Event('click');
para.dispatchEvent(event);
複製代碼

上面代碼在當前節點觸發了click事件。

若是dispatchEvent方法的參數爲空,或者不是一個有效的事件對象,將報錯。

事件模型

監聽函數

瀏覽器的事件模型,就是經過監聽函數(listener)對事件作出反應。事件發生後,瀏覽器監聽到了這個事件,就會執行對應的監聽函數。這是事件驅動編程模式(event-driven)的主要編程方式。

JavaScript 有三種方法,能夠爲事件綁定監聽函數。

HTML 的 on- 屬性

HTML 語言容許在元素的屬性中,直接定義某些事件的監聽代碼。

<body onload="doSomething()">
<div onclick="console.log('觸發事件')">
複製代碼

上面代碼爲body節點的load事件、div節點的click事件,指定了監聽代碼。一旦事件發生,就會執行這段代碼。

元素的事件監聽屬性,都是on加上事件名,好比onload就是on + load,表示load事件的監聽代碼。

注意,這些屬性的值是將會執行的代碼,而不是一個函數。

<!-- 正確 -->
<body onload="doSomething()">
複製代碼

一旦指定的事件發生,on-屬性的值是原樣傳入 JavaScript 引擎執行。所以若是要執行函數,不要忘記加上一對圓括號使用這個方法只會在冒泡階段觸發

直接設置on-屬性,與經過元素節點的setAttribute方法設置on-屬性,效果是同樣的。

el.setAttribute('onclick', 'doSomething()');
// 等同於
// <Element onclick="doSomething()">
複製代碼

元素節點的事件屬性

元素節點對象的事件屬性,一樣能夠指定監聽函數。

window.onload = doSomething;

div.onclick = function (event) {
  console.log('觸發事件');
};
複製代碼

使用這個方法指定的監聽函數,也是隻會在冒泡階段觸發。

注意,這種方法與 HTML 的on-屬性的差別是,它的值是函數名(doSomething),而不像後者,必須給出完整的監聽代碼(doSomething())。

EventTarget.addEventListener()

全部 DOM 節點實例都有addEventListener方法,用來爲該節點定義事件的監聽函數。

window.addEventListener('load', doSomething, false);
複製代碼

addEventListener方法的詳細介紹,參見上面EventTarget 接口。

小結

上面三種方法,第一種「HTML 的 on- 屬性」,違反了 HTML 與 JavaScript 代碼相分離的原則,將二者寫在一塊兒,不利於代碼分工,所以不推薦使用。

第二種「元素節點的事件屬性」的缺點在於,同一個事件只能定義一個監聽函數,也就是說,若是定義兩次onclick屬性,後一次定義會覆蓋前一次。所以,也不推薦使用。

第三種EventTarget.addEventListener是推薦的指定監聽函數的方法。它有以下優勢:

  • 同一個事件能夠添加多個監聽函數。
  • 可以指定在哪一個階段(捕獲階段仍是冒泡階段)觸發監聽函數。
  • 除了 DOM 節點,其餘對象(好比windowXMLHttpRequest等)也有這個接口,它等因而整個 JavaScript 統一的監聽函數接口。

this 的指向

監聽函數內部的this指向觸發事件的那個元素節點。三種監聽函數的寫法,this的指向均是如此。

// HTML 代碼以下 寫法一
// <button id="btn" onclick="console.log(this.id)">點擊</button> // btn
var btn = document.getElementById('btn');

// 寫法二
btn.onclick = function () {
  console.log(this.id); // btn
};

// 寫法三
btn.addEventListener(
  'click',
  function (e) {
    console.log(this.id); // btn
  },
  false
);
複製代碼

上面兩種寫法,點擊按鈕之後也是輸出btn

事件的傳播

一個事件發生後,會在子元素和父元素之間傳播(propagation)。這種傳播分紅三個階段。

  • 第一階段:從window對象傳導到目標節點(上層傳到底層),稱爲「捕獲階段」(capture phase)。
  • 第二階段:在目標節點上觸發,稱爲「目標階段」(target phase)。
  • 第三階段:從目標節點傳導回window對象(從底層傳回上層),稱爲「冒泡階段」(bubbling phase)。

這種三階段的傳播模型,使得同一個事件會在多個節點上觸發。

<div>
  <p>點擊</p>
</div>
複製代碼

上面代碼中,<div>節點之中有一個<p>節點。

若是對這兩個節點,都設置click事件的監聽函數(每一個節點的捕獲階段和冒泡階段,各設置一個監聽函數),共計設置四個監聽函數。而後,對<p>點擊,click事件會觸發四次。

var phases = {
  1: 'capture',
  2: 'target',
  3: 'bubble'
};

var div = document.querySelector('div');
var p = document.querySelector('p');

div.addEventListener('click', callback, true);
p.addEventListener('click', callback, true);
div.addEventListener('click', callback, false);
p.addEventListener('click', callback, false);

function callback(event) {
  var tag = event.currentTarget.tagName;
  var phase = phases[event.eventPhase];
  console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'");
}

// 點擊之後的結果
// Tag: 'DIV'. EventPhase: 'capture'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'DIV'. EventPhase: 'bubble'
複製代碼

上面代碼表示,click事件被觸發了四次:<div>節點的捕獲階段和冒泡階段各1次,<p>節點的目標階段觸發了2次。

  1. 捕獲階段:事件從<div><p>傳播時,觸發<div>click事件;
  2. 目標階段:事件從<div>到達<p>時,觸發<p>click事件;
  3. 冒泡階段:事件從<p>傳回<div>時,再次觸發<div>click事件。

其中,<p>節點有兩個監聽函數(addEventListener方法第三個參數的不一樣,會致使綁定兩個監聽函數),所以它們都會由於click事件觸發一次。因此,<p>會在target階段有兩次輸出。

事件傳播的最上層對象是window,接着依次是documenthtmldocument.documentElement)和bodydocument.body)。也就是說,上例的事件傳播順序,在捕獲階段依次爲windowdocumenthtmlbodydivp,在冒泡階段依次爲pdivbodyhtmldocumentwindow

事件的代理

因爲事件會在冒泡階段向上傳播到父節點,所以能夠把子節點的監聽函數定義在父節點上,由父節點的監聽函數統一處理多個子元素的事件。這種方法叫作事件的代理(delegation)。

var ul = document.querySelector('ul');

ul.addEventListener('click', function (event) {
  if (event.target.tagName.toLowerCase() === 'li') {
    // some code
  }
});
複製代碼

上面代碼中,click事件的監聽函數定義在<ul>節點,可是實際上,它處理的是子節點<li>click事件。這樣作的好處是,只要定義一個監聽函數,就能處理多個子節點的事件,而不用在每一個<li>節點上定義監聽函數。並且之後再添加子節點,監聽函數依然有效。

若是但願事件到某個節點爲止,再也不傳播,可使用事件對象的stopPropagation方法。

// 事件傳播到 p 元素後,就再也不向下傳播了
p.addEventListener('click', function (event) {
  event.stopPropagation();
}, true);

// 事件冒泡到 p 元素後,就再也不向上冒泡了
p.addEventListener('click', function (event) {
  event.stopPropagation();
}, false);
複製代碼

上面代碼中,stopPropagation方法分別在捕獲階段和冒泡階段,阻止了事件的傳播。

可是,stopPropagation方法只會阻止事件的傳播,不會阻止該事件觸發<p>節點的其餘click事件的監聽函數。也就是說,不是完全取消click事件。

若是想要完全取消該事件,再也不觸發後面全部click的監聽函數,可使用stopImmediatePropagation方法。

p.addEventListener('click', function (event) {
  event.stopImmediatePropagation();
  console.log(1);
});

p.addEventListener('click', function(event) {
  // 不會被觸發
  console.log(2);
});
複製代碼

上面代碼中,stopImmediatePropagation方法能夠完全取消這個事件,使得後面綁定的全部click監聽函數都再也不觸發。因此,只會輸出1,不會輸出2。

相關文章
相關標籤/搜索