JavaScript 事件模型

2023-03-20 15:47 更新

監(jiān)聽(tīng)函數(shù)

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

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

HTML 的 on- 屬性

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

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

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

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

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

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

<!-- 錯(cuò)誤 -->
<body onload="doSomething">

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

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

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

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

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

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

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

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

window.onload = doSomething;

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

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

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

EventTarget.addEventListener()

所有 DOM 節(jié)點(diǎn)實(shí)例都有addEventListener方法,用來(lái)為該節(jié)點(diǎn)定義事件的監(jiān)聽(tīng)函數(shù)。

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

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

小結(jié) #

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

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

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

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

this 的指向 #

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

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

執(zhí)行上面代碼,點(diǎn)擊后會(huì)輸出btn。

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

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

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

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

上面兩種寫(xiě)法,點(diǎn)擊按鈕以后也是輸出btn

事件的傳播 #

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

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

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

<div>
  <p>點(diǎn)擊</p>
</div>

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

如果對(duì)這兩個(gè)節(jié)點(diǎn),都設(shè)置click事件的監(jiān)聽(tīng)函數(shù)(每個(gè)節(jié)點(diǎn)的捕獲階段和冒泡階段,各設(shè)置一個(gè)監(jiān)聽(tīng)函數(shù)),共計(jì)設(shè)置四個(gè)監(jiān)聽(tīng)函數(shù)。然后,對(duì)<p>點(diǎn)擊,click事件會(huì)觸發(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 + "'");
}

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

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

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

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

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

事件傳播的最上層對(duì)象是window,接著依次是document,htmldocument.documentElement)和bodydocument.body)。也就是說(shuō),上例的事件傳播順序,在捕獲階段依次為window、document、htmlbody、divp,在冒泡階段依次為pdiv、body、html、document、window。

事件的代理 #

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)