2022 年 9 月 21 日

瀏覽器事件簡介

事件是表示某事已發生的訊號。所有 DOM 節點都會產生此類訊號(但事件不限於 DOM)。

以下列出最實用的 DOM 事件清單,供您參考

滑鼠事件

  • click – 當滑鼠按一下元素時(觸控螢幕裝置會在輕觸時產生此事件)。
  • contextmenu – 當滑鼠在元素上按右鍵時。
  • mouseover / mouseout – 當滑鼠游標移到 / 離開元素時。
  • mousedown / mouseup – 當滑鼠按鈕在元素上按下 / 釋放時。
  • mousemove – 當滑鼠移動時。

鍵盤事件

  • keydownkeyup – 當鍵盤按鍵按下和釋放時。

表單元素事件

  • submit – 當訪客提交 <form> 時。
  • focus – 當訪客聚焦在元素上,例如 <input>

文件事件

  • DOMContentLoaded – 當 HTML 已載入和處理完畢,DOM 已完全建立時。

CSS 事件

  • transitionend – 當 CSS 動畫完成時。

還有許多其他事件。我們將在後面的章節中深入探討特定事件。

事件處理常式

為了對事件做出反應,我們可以指定一個處理常式 – 一個在事件發生時執行的函式。

處理常式是一種在使用者操作時執行 JavaScript 程式碼的方法。

有幾種指定處理常式的方法。讓我們從最簡單的方法開始。

HTML 屬性

可以在 HTML 中使用名為 on<event> 的屬性設定處理常式。

例如,要為 input 指定 click 處理常式,我們可以使用 onclick,如下所示

<input value="Click me" onclick="alert('Click!')" type="button">

滑鼠按一下時,onclick 內的程式碼就會執行。

請注意,在 onclick 內我們使用單引號,因為屬性本身是用雙引號。如果我們忘記程式碼在屬性內,並在其中使用雙引號,如下所示:onclick="alert("Click!")",則它將無法正常運作。

HTML 屬性不是撰寫大量程式碼的便利位置,因此我們最好建立一個 JavaScript 函式並在那裡呼叫它。

這裡的按一下會執行函式 countRabbits()

<script>
  function countRabbits() {
    for(let i=1; i<=3; i++) {
      alert("Rabbit number " + i);
    }
  }
</script>

<input type="button" onclick="countRabbits()" value="Count rabbits!">

如我們所知,HTML 屬性名稱不區分大小寫,因此 ONCLICKonClickonCLICK… 一樣有效,但通常屬性會使用小寫:onclick

DOM 屬性

我們可以使用 DOM 屬性 on<event> 指定處理常式。

例如,elem.onclick

<input id="elem" type="button" value="Click me">
<script>
  elem.onclick = function() {
    alert('Thank you');
  };
</script>

如果處理常式是使用 HTML 屬性指定的,則瀏覽器會讀取它,從屬性內容建立一個新函式,並將它寫入 DOM 屬性。

所以這種方式實際上與前一種方式相同。

這兩個程式碼片段運作方式相同

  1. 只有 HTML

    <input type="button" onclick="alert('Click!')" value="Button">
  2. HTML + JS

    <input type="button" id="button" value="Button">
    <script>
      button.onclick = function() {
        alert('Click!');
      };
    </script>

在第一個範例中,HTML 屬性用於初始化 button.onclick,而在第二個範例中,則是用腳本,這就是全部的差別。

由於只有一個 onclick 屬性,因此我們無法指定多個事件處理常式。

在以下範例中,使用 JavaScript 新增處理常式會覆寫現有的處理常式

<input type="button" id="elem" onclick="alert('Before')" value="Click me">
<script>
  elem.onclick = function() { // overwrites the existing handler
    alert('After'); // only this will be shown
  };
</script>

若要移除處理常式,請指定 elem.onclick = null

存取元素:this

處理常式內 this 的值是元素。就是具有處理常式的元素。

在以下程式碼中,button 使用 this.innerHTML 顯示其內容

<button onclick="alert(this.innerHTML)">Click me</button>

可能的錯誤

如果您開始使用事件,請注意一些細微差別。

我們可以將現有函式設定為處理常式

function sayThanks() {
  alert('Thanks!');
}

elem.onclick = sayThanks;

但請小心:函式應指定為 sayThanks,而不是 sayThanks()

// right
button.onclick = sayThanks;

// wrong
button.onclick = sayThanks();

如果我們加上括號,則 sayThanks() 會變成函式呼叫。因此,最後一行實際上會取得函式執行結果,也就是 undefined(因為函式沒有傳回任何內容),並將其指定給 onclick。這無法運作。

…另一方面,在標記中,我們確實需要括號

<input type="button" id="button" onclick="sayThanks()">

這個差別很容易解釋。當瀏覽器讀取屬性時,它會從屬性內容建立一個主體具有處理常式函式的函式。

因此,標記會產生這個屬性

button.onclick = function() {
  sayThanks(); // <-- the attribute content goes here
};

請勿對處理常式使用 setAttribute

這樣的呼叫無法運作

// a click on <body> will generate errors,
// because attributes are always strings, function becomes a string
document.body.setAttribute('onclick', function() { alert(1) });

DOM 屬性會區分大小寫。

將處理常式指定給 elem.onclick,而不是 elem.ONCLICK,因為 DOM 屬性會區分大小寫。

addEventListener

上述指定處理常式方式的基本問題是,我們無法將多個處理常式指定給一個事件

假設我們的程式碼一部分想要在按一下時突顯按鈕,而另一部分想要在按一下時顯示訊息。

我們想要為此指定兩個事件處理常式。但新的 DOM 屬性會覆寫現有的屬性

input.onclick = function() { alert(1); }
// ...
input.onclick = function() { alert(2); } // replaces the previous handler

網頁標準開發人員很早以前就了解到這一點,並建議使用特殊方法 addEventListenerremoveEventListener 來管理處理常式,這些方法不受此限制。

新增處理常式的語法

element.addEventListener(event, handler, [options]);
事件
事件名稱,例如 "click"
處理常式
處理常式函式。
選項
具有下列屬性的附加選用物件
  • once:若為 true,則觸發後會自動移除監聽器。
  • capture:處理事件的階段,稍後會於 浮動和擷取 章節中說明。基於歷史原因,options 也可能是 false/true,這與 {capture: false/true} 相同。
  • passive:若為 true,則處理常式不會呼叫 preventDefault(),我們稍後會於 瀏覽器預設動作 中說明。

若要移除處理常式,請使用 removeEventListener

element.removeEventListener(event, handler, [options]);
移除需要相同的函式

若要移除處理常式,我們應傳遞與指定函式完全相同的函式。

這無法運作

elem.addEventListener( "click" , () => alert('Thanks!'));
// ....
elem.removeEventListener( "click", () => alert('Thanks!'));

處理常式不會被移除,因為 removeEventListener 會取得另一個函式,其具有相同的程式碼,但這並不重要,因為它是不同的函式物件。

以下是正確的方式

function handler() {
  alert( 'Thanks!' );
}

input.addEventListener("click", handler);
// ....
input.removeEventListener("click", handler);

請注意,如果我們未將函式儲存在變數中,則無法移除它。沒有辦法「讀回」由 addEventListener 指定的處理常式。

多次呼叫 addEventListener 可讓它新增多個處理常式,如下所示

<input id="elem" type="button" value="Click me"/>

<script>
  function handler1() {
    alert('Thanks!');
  };

  function handler2() {
    alert('Thanks again!');
  }

  elem.onclick = () => alert("Hello");
  elem.addEventListener("click", handler1); // Thanks!
  elem.addEventListener("click", handler2); // Thanks again!
</script>

如上例所示,我們可以使用 DOM 屬性和 addEventListener 同時 設定處理常式。但通常我們只會使用其中一種方式。

對於某些事件,處理常式僅能使用 addEventListener

有些事件無法透過 DOM 屬性指定。只能使用 addEventListener

例如,DOMContentLoaded 事件,會在載入文件並建立 DOM 時觸發。

// will never run
document.onDOMContentLoaded = function() {
  alert("DOM built");
};
// this way it works
document.addEventListener("DOMContentLoaded", function() {
  alert("DOM built");
});

因此,addEventListener 較為通用。儘管如此,此類事件是例外,而非常態。

事件物件

若要妥善處理事件,我們會希望進一步瞭解發生了什麼事。不只是「按一下」或「按下鍵」,而是指標座標為何?按下了哪個鍵?等等。

當事件發生時,瀏覽器會建立一個事件物件,將詳細資訊放入其中,並將其作為引數傳遞給處理常式。

以下是如何從事件物件取得指標座標的範例

<input type="button" value="Click me" id="elem">

<script>
  elem.onclick = function(event) {
    // show event type, element and coordinates of the click
    alert(event.type + " at " + event.currentTarget);
    alert("Coordinates: " + event.clientX + ":" + event.clientY);
  };
</script>

event 物件的一些屬性

event.type
事件類型,這裡是 "click"
event.currentTarget
處理事件的元素。這與 this 完全相同,除非處理常式是箭頭函式,或其 this 繫結到其他項目,否則我們可以從 event.currentTarget 取得元素。
event.clientX / event.clientY
指標事件的游標相對於視窗的座標。

還有更多屬性。其中許多屬性取決於事件類型:鍵盤事件有一組屬性,指標事件有另一組屬性,我們會在探討不同事件的詳細資訊時再研究它們。

事件物件在 HTML 處理常式中也可用

如果我們在 HTML 中指定處理常式,我們也可以使用 event 物件,如下所示

<input type="button" onclick="alert(event.type)" value="Event type">

這是可能的,因為當瀏覽器讀取屬性時,它會建立一個類似這樣的處理常式:function(event) { alert(event.type) }。也就是說:它的第一個引數稱為 "event",而主體則取自屬性。

物件處理常式:handleEvent

我們可以使用 addEventListener 不僅指定函式,還可以指定物件作為事件處理常式。當事件發生時,會呼叫其 handleEvent 方法。

例如

<button id="elem">Click me</button>

<script>
  let obj = {
    handleEvent(event) {
      alert(event.type + " at " + event.currentTarget);
    }
  };

  elem.addEventListener('click', obj);
</script>

正如我們所見,當 addEventListener 接收到物件作為處理常式時,它會在事件發生時呼叫 obj.handleEvent(event)

我們也可以使用自訂類別的物件,如下所示

<button id="elem">Click me</button>

<script>
  class Menu {
    handleEvent(event) {
      switch(event.type) {
        case 'mousedown':
          elem.innerHTML = "Mouse button pressed";
          break;
        case 'mouseup':
          elem.innerHTML += "...and released.";
          break;
      }
    }
  }

  let menu = new Menu();

  elem.addEventListener('mousedown', menu);
  elem.addEventListener('mouseup', menu);
</script>

這裡同一個物件處理這兩個事件。請注意,我們需要使用 addEventListener 明確設定要監聽的事件。menu 物件在此處僅取得 mousedownmouseup,而不是任何其他類型的事件。

handleEvent 方法不必自己執行所有工作。它可以呼叫其他特定於事件的方法,如下所示

<button id="elem">Click me</button>

<script>
  class Menu {
    handleEvent(event) {
      // mousedown -> onMousedown
      let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
      this[method](event);
    }

    onMousedown() {
      elem.innerHTML = "Mouse button pressed";
    }

    onMouseup() {
      elem.innerHTML += "...and released.";
    }
  }

  let menu = new Menu();
  elem.addEventListener('mousedown', menu);
  elem.addEventListener('mouseup', menu);
</script>

現在事件處理常式已清楚區分,這可能更容易支援。

摘要

有 3 種指定事件處理常式的方法

  1. HTML 屬性:onclick="..."
  2. DOM 屬性:elem.onclick = function
  3. 方法:elem.addEventListener(event, handler[, phase]) 用於新增,removeEventListener 用於移除。

HTML 屬性使用較少,因為 HTML 標籤中間的 JavaScript 看起來有點奇怪和陌生。而且也無法在其中撰寫大量程式碼。

DOM 屬性可以使用,但我們無法為特定事件指定多個處理常式。在許多情況下,此限制並非迫切。

最後一種方法最靈活,但也是撰寫時間最長的方法。只有少數事件只能使用它,例如 transitionendDOMContentLoaded(將會介紹)。此外,addEventListener 支援物件作為事件處理常式。在這種情況下,在事件發生時會呼叫方法 handleEvent

不論您如何指定處理常式,它都會將事件物件作為第一個引數取得。該物件包含有關發生事件的詳細資料。

我們將在後續章節中進一步了解事件的一般資訊和不同類型的事件。

任務

重要性:5

將 JavaScript 新增到 button,讓 <div id="text"> 在我們按一下它時消失。

示範

為任務開啟沙盒。

重要性:5

建立一個在按一下時隱藏自己的按鈕。

如下所示:

可以在處理常式中使用 this 來參考此處的「元素本身」

<input type="button" onclick="this.hidden=true" value="Click to hide">
重要性:5

變數中有一個按鈕。它沒有處理常式。

在以下程式碼之後按一下時,哪些處理常式會執行?哪些警示會顯示?

button.addEventListener("click", () => alert("1"));

button.removeEventListener("click", () => alert("1"));

button.onclick = () => alert(2);

答案:12

第一個處理常式會觸發,因為它未被 removeEventListener 移除。若要移除處理常式,我們需要傳遞與指定處理常式完全相同的函式。而在程式碼中傳遞了一個新的函式,它看起來相同,但仍然是另一個函式。

若要移除函式物件,我們需要儲存對它的參考,如下所示

function handler() {
  alert(1);
}

button.addEventListener("click", handler);
button.removeEventListener("click", handler);

處理常式 button.onclick 獨立運作,並額外提供 addEventListener

重要性:5

將球移動到場地另一端,按一下即可。如下所示

需求

  • 按一下時,球的中心應準確出現在指標下方(如果可能,不超過場地邊緣)。
  • 歡迎使用 CSS 動畫。
  • 球不能越過場地界線。
  • 當頁面捲動時,任何東西都不應該中斷。

備註

  • 程式碼也應該能使用不同的球和場地大小,不受限於任何固定值。
  • 使用屬性 event.clientX/event.clientY 來取得點擊座標。

為任務開啟沙盒。

首先,我們需要選擇一個定位球的方法。

我們不能對其使用 position:fixed,因為捲動頁面會將球從場地移出。

所以我們應該使用 position:absolute,並讓 field 本身定位,以讓定位真正穩固。

然後球將相對於場地定位

#field {
  width: 200px;
  height: 150px;
  position: relative;
}

#ball {
  position: absolute;
  left: 0; /* relative to the closest positioned ancestor (field) */
  top: 0;
  transition: 1s all; /* CSS animation for left/top makes the ball fly */
}

接下來,我們需要指定正確的 ball.style.left/top。它們現在包含相對於場地的座標。

以下是圖片

我們有 event.clientX/clientY - 相對於視窗的點擊座標。

若要取得點擊的相對於場地的 left 座標,我們可以減去場地的左邊緣和邊框寬度

let left = event.clientX - fieldCoords.left - field.clientLeft;

通常,ball.style.left 表示「元素的左邊緣」(球)。因此,如果我們指定該 left,則球的邊緣(而非中心)會在滑鼠游標下方。

我們需要將球向左移動一半寬度,向上移動一半高度,以使其置中。

因此,最後的 left 將會是

let left = event.clientX - fieldCoords.left - field.clientLeft - ball.offsetWidth/2;

垂直座標使用相同的邏輯計算。

請注意,當我們存取 ball.offsetWidth 時,必須知道球的寬度/高度。應該在 HTML 或 CSS 中指定。

在沙盒中開啟解決方案。

重要性:5

建立一個在點擊時開啟/收合的選單

P.S. 原始文件的 HTML/CSS 必須修改。

為任務開啟沙盒。

HTML/CSS

首先,讓我們建立 HTML/CSS。

選單是頁面上的獨立圖形元件,因此最好將其放入單一 DOM 元素中。

選單項目清單可以佈置成清單 ul/li

以下是範例結構

<div class="menu">
  <span class="title">Sweeties (click me)!</span>
  <ul>
    <li>Cake</li>
    <li>Donut</li>
    <li>Honey</li>
  </ul>
</div>

我們對標題使用 <span>,因為 <div> 對其具有隱含的 display:block,它將佔用 100% 的水平寬度。

像這樣

<div style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</div>

因此,如果我們對其設定 onclick,它將會捕捉到文字右方的點擊。

由於 <span> 具有隱含的 display: inline,它只會佔用剛好容納所有文字的空間

<span style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</span>

切換選單

切換選單應變更箭頭並顯示/隱藏選單清單。

所有這些變更都由 CSS 完美處理。在 JavaScript 中,我們應透過新增/移除類別 .open 標記選單的目前狀態。

沒有它,選單將關閉

.menu ul {
  margin: 0;
  list-style: none;
  padding-left: 20px;
  display: none;
}

.menu .title::before {
  content: '▶ ';
  font-size: 80%;
  color: green;
}

…而加上 .open 後,箭頭會變更,清單會顯示

.menu.open .title::before {
  content: '▼ ';
}

.menu.open ul {
  display: block;
}

在沙盒中開啟解決方案。

重要性:5

有一個訊息清單。

使用 JavaScript 為每個訊息的右上角新增關閉按鈕。

結果應如下所示

為任務開啟沙盒。

若要新增按鈕,我們可以使用 position:absolute(並將窗格設為 position:relative)或 float:rightfloat:right 的好處是按鈕絕不會與文字重疊,但 position:absolute 提供更多彈性。因此,選擇權在您。

然後,對於每個窗格,程式碼可以如下所示

pane.insertAdjacentHTML("afterbegin", '<button class="remove-button">[x]</button>');

然後,<button> 會變成 pane.firstChild,因此我們可以這樣為它新增處理常式

pane.firstChild.onclick = () => pane.remove();

在沙盒中開啟解決方案。

重要性:4

建立「輪播」—一個可以透過按一下箭頭來捲動的圖片功能表。

稍後,我們可以為它新增更多功能:無限捲動、動態載入等。

附註:對於這個任務,HTML/CSS 結構實際上是解決方案的 90%。

為任務開啟沙盒。

圖片功能表可以表示為圖片 <img>ul/li 清單。

通常,這樣的功能表很寬,但我們會在周圍放置一個固定大小的 <div> 來「裁剪」它,這樣只會看到功能表的一部分

若要讓清單水平顯示,我們需要套用正確的 CSS 屬性給 <li>,例如 display: inline-block

對於 <img>,我們也應調整 display,因為預設為 inline。在 inline 元素下方保留了額外的空間給「字母尾巴」,因此我們可以使用 display:block 來移除它。

若要捲動,我們可以移動 <ul>。有許多方法可以做到這一點,例如透過變更 margin-left 或(效能較佳)使用 transform: translateX()

外部 <div> 有固定寬度,因此會裁剪「額外的」圖片。

整個輪播是頁面上的獨立「圖形元件」,因此我們最好將它包覆在單一的 <div class="carousel"> 中,並設定其內部的樣式。

在沙盒中開啟解決方案。

教學課程地圖

留言

留言前請閱讀…
  • 如果你有改善建議 - 請 提交 GitHub 問題 或發起拉取請求,而非留言。
  • 如果你無法理解文章中的內容 - 請詳細說明。
  • 若要插入少數幾個字的程式碼,請使用 <code> 標籤,若要插入多行,請將它們包覆在 <pre> 標籤中,若要插入超過 10 行,請使用沙盒 (plnkrjsbincodepen…)