JavaScript 事件模型

2023-03-20 15:47 更新

監(jiān)聽函數(shù)

瀏覽器的事件模型,就是通過監(jiān)聽函數(shù)(listener)對事件做出反應(yīng)。事件發(fā)生后,瀏覽器監(jiān)聽到了這個事件,就會執(zhí)行對應(yīng)的監(jiān)聽函數(shù)。這是事件驅(qū)動編程模式(event-driven)的主要編程方式。

JavaScript 有三種方法,可以為事件綁定監(jiān)聽函數(shù)。

HTML 的 on- 屬性

HTML 語言允許在元素的屬性中,直接定義某些事件的監(jiān)聽代碼。

<body onload="doSomething()">
<div onclick="console.log('觸發(fā)事件')">

上面代碼為body節(jié)點的load事件、div節(jié)點的click事件,指定了監(jiān)聽代碼。一旦事件發(fā)生,就會執(zhí)行這段代碼。

元素的事件監(jiān)聽屬性,都是on加上事件名,比如onload就是on + load,表示load事件的監(jiān)聽代碼。

注意,這些屬性的值是將會執(zhí)行的代碼,而不是一個函數(shù)。

<!-- 正確 -->
<body onload="doSomething()">

<!-- 錯誤 -->
<body onload="doSomething">

一旦指定的事件發(fā)生,on-屬性的值是原樣傳入 JavaScript 引擎執(zhí)行。因此如果要執(zhí)行函數(shù),不要忘記加上一對圓括號。

使用這個方法指定的監(jiān)聽代碼,只會在冒泡階段觸發(fā)。

<div onclick="console.log(2)">
  <button onclick="console.log(1)">點擊</button>
</div>

上面代碼中,<button><div>的子元素。<button>click事件,也會觸發(fā)<div>click事件。由于on-屬性的監(jiān)聽代碼,只在冒泡階段觸發(fā),所以點擊結(jié)果是先輸出1,再輸出2,即事件從子元素開始冒泡到父元素。

直接設(shè)置on-屬性,與通過元素節(jié)點的setAttribute方法設(shè)置on-屬性,效果是一樣的。

el.setAttribute('onclick', 'doSomething()');
// 等同于
// <Element onclick="doSomething()">

元素節(jié)點的事件屬性

元素節(jié)點對象的事件屬性,同樣可以指定監(jiān)聽函數(shù)。

window.onload = doSomething;

div.onclick = function (event) {
  console.log('觸發(fā)事件');
};

使用這個方法指定的監(jiān)聽函數(shù),也是只會在冒泡階段觸發(fā)。

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

EventTarget.addEventListener()

所有 DOM 節(jié)點實例都有addEventListener方法,用來為該節(jié)點定義事件的監(jiān)聽函數(shù)。

window.addEventListener('load', doSomething, false);

addEventListener方法的詳細(xì)介紹,參見EventTarget章節(jié)。

小結(jié) #

上面三種方法,第一種“HTML 的 on- 屬性”,違反了 HTML 與 JavaScript 代碼相分離的原則,將兩者寫在一起,不利于代碼分工,因此不推薦使用。

第二種“元素節(jié)點的事件屬性”的缺點在于,同一個事件只能定義一個監(jiān)聽函數(shù),也就是說,如果定義兩次onclick屬性,后一次定義會覆蓋前一次。因此,也不推薦使用。

第三種EventTarget.addEventListener是推薦的指定監(jiān)聽函數(shù)的方法。它有如下優(yōu)點:

  • 同一個事件可以添加多個監(jiān)聽函數(shù)。
  • 能夠指定在哪個階段(捕獲階段還是冒泡階段)觸發(fā)監(jiān)聽函數(shù)。
  • 除了 DOM 節(jié)點,其他對象(比如window、XMLHttpRequest等)也有這個接口,它等于是整個 JavaScript 統(tǒng)一的監(jiān)聽函數(shù)接口。

this 的指向 #

監(jiān)聽函數(shù)內(nèi)部的this指向觸發(fā)事件的那個元素節(jié)點。

<button id="btn" onclick="console.log(this.id)">點擊</button>

執(zhí)行上面代碼,點擊后會輸出btn。

其他兩種監(jiān)聽函數(shù)的寫法,this的指向也是如此。

// HTML 代碼如下
// <button id="btn">點擊</button>
var btn = document.getElementById('btn');

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

// 寫法二
btn.addEventListener(
  'click',
  function (e) {
    console.log(this.id);
  },
  false
);

上面兩種寫法,點擊按鈕以后也是輸出btn。

事件的傳播 #

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

  • 第一階段:從window對象傳導(dǎo)到目標(biāo)節(jié)點(上層傳到底層),稱為“捕獲階段”(capture phase)。
  • 第二階段:在目標(biāo)節(jié)點上觸發(fā),稱為“目標(biāo)階段”(target phase)。
  • 第三階段:從目標(biāo)節(jié)點傳導(dǎo)回window對象(從底層傳回上層),稱為“冒泡階段”(bubbling phase)。

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

<div>
  <p>點擊</p>
</div>

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

如果對這兩個節(jié)點,都設(shè)置click事件的監(jiān)聽函數(shù)(每個節(jié)點的捕獲階段和冒泡階段,各設(shè)置一個監(jiān)聽函數(shù)),共計設(shè)置四個監(jiān)聽函數(shù)。然后,對<p>點擊,click事件會觸發(fā)四次。

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 + "'");
}

// 點擊以后的結(jié)果
// Tag: 'DIV'. EventPhase: 'capture'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'DIV'. EventPhase: 'bubble'

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

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

其中,<p>節(jié)點有兩個監(jiān)聽函數(shù)(addEventListener方法第三個參數(shù)的不同,會導(dǎo)致綁定兩個監(jiān)聽函數(shù)),因此它們都會因為click事件觸發(fā)一次。所以,<p>會在target階段有兩次輸出。

注意,瀏覽器總是假定click事件的目標(biāo)節(jié)點,就是點擊位置嵌套最深的那個節(jié)點(本例是<div>節(jié)點里面的<p>節(jié)點)。所以,<p>節(jié)點的捕獲階段和冒泡階段,都會顯示為target階段。

事件傳播的最上層對象是window,接著依次是documenthtmldocument.documentElement)和bodydocument.body)。也就是說,上例的事件傳播順序,在捕獲階段依次為window、documenthtml、bodydiv、p,在冒泡階段依次為p、divbody、html、documentwindow。

事件的代理 #

由于事件會在冒泡階段向上傳播到父節(jié)點,因此可以把子節(jié)點的監(jiān)聽函數(shù)定義在父節(jié)點上,由父節(jié)點的監(jiān)聽函數(shù)統(tǒng)一處理多個子元素的事件。這種方法叫做事件的代理(delegation)。

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

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

上面代碼中,click事件的監(jiān)聽函數(shù)定義在<ul>節(jié)點,但是實際上,它處理的是子節(jié)點<li>click事件。這樣做的好處是,只要定義一個監(jiān)聽函數(shù),就能處理多個子節(jié)點的事件,而不用在每個<li>節(jié)點上定義監(jiān)聽函數(shù)。而且以后再添加子節(jié)點,監(jiān)聽函數(shù)依然有效。

如果希望事件到某個節(jié)點為止,不再傳播,可以使用事件對象的stopPropagation方法。

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

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

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

但是,stopPropagation方法只會阻止事件的傳播,不會阻止該事件觸發(fā)<p>節(jié)點的其他click事件的監(jiān)聽函數(shù)。也就是說,不是徹底取消click事件。

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

p.addEventListener('click', function(event) {
  // 會觸發(fā)
  console.log(2);
});

上面代碼中,p元素綁定了兩個click事件的監(jiān)聽函數(shù)。stopPropagation方法只能阻止這個事件的傳播,不能取消這個事件,因此,第二個監(jiān)聽函數(shù)會觸發(fā)。輸出結(jié)果會先是1,然后是2。

如果想要徹底取消該事件,不再觸發(fā)后面所有click的監(jiān)聽函數(shù),可以使用stopImmediatePropagation方法。

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

p.addEventListener('click', function(event) {
  // 不會被觸發(fā)
  console.log(2);
});

上面代碼中,stopImmediatePropagation方法可以徹底取消這個事件,使得后面綁定的所有click監(jiān)聽函數(shù)都不再觸發(fā)。所以,只會輸出1,不會輸出2。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號