Home
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

4

Local AI

--

Uedu Code

--

Taipei AQI 57 · Taichung AQI 74 · Tainan AQI 63 · Kaohsiung AQI 64
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 19

事件處理

addEventListener、滑鼠與鍵盤事件、事件物件與冒泡
Interaction 25 minute(s)

Learning Objectives

  • 說明事件、事件物件與事件處理器(事件監聽器),並比較 HTML 屬性、onclick 屬性與 addEventListener 三種寫法
  • 用 addEventListener 加入多個處理器,用 removeEventListener 移除,並說明處理器裡 this 與 e.currentTarget 的意思
  • 使用事件物件的 type、target、clientX、key 等屬性,處理 click、mouseenter、keydown、input 等常用事件
  • 說明事件流的捕獲、目標與冒泡階段,並用 stopPropagation、preventDefault 與事件委派處理事件

事件:網頁對使用者的回應

使用者點按鈕、移動滑鼠、在輸入框打字,瀏覽器都會產生一個事件(event)。網頁要有互動,就是「某個事件發生時,執行某段程式」。

名詞 意思 例子
事件(event) 發生了什麼事 click、mouseover、keydown、input、submit
事件物件(event object) 這次事件的詳細資訊,瀏覽器自動傳給處理器 點在哪裡(clientX)、按了哪個鍵(key)、發生在哪個元素(target)
事件處理器(event handler) 事件發生時要執行的函式,也叫事件監聽器(event listener) function (e) { … }

事件不只來自使用者,也可能來自瀏覽器本身(網頁載入完成的 load)、非同步工作(下載完成),或由程式自己觸發。

三種加上事件處理器的寫法

<!-- 1. HTML 屬性(嵌入式,DOM 0 級) -->
<button onclick="sayHi()">按我</button>
// 2. 指定元素的 on… 屬性(傳統寫法,DOM 0 級):名稱一律小寫
btn.onclick = sayHi;
btn.onclick = null;               // 移除

// 3. addEventListener(W3C 標準,DOM 2 級):事件名稱不加 on
btn.addEventListener('click', sayHi);
btn.removeEventListener('click', sayHi);
HTML 屬性 onclick 屬性 addEventListener
程式寫在哪裡 HTML 裡 JavaScript 裡 JavaScript 裡
同一個事件幾個處理器 一個 一個(再指定一次會取代前一個) 多個,依加入順序執行
可以選擇捕獲階段 不行 不行 可以(第三個參數)

HTML 屬性把程式和 HTML 混在一起,不好除錯與維護,看得懂即可;新的程式一律用 addEventListener。 (很舊的 IE 瀏覽器有自己的 attachEvent,現在已經不需要了。)

addEventListener(事件名稱, 處理器, 第三個參數) 的規則:

  • 第三個參數是 true 表示在捕獲階段執行,省略或 false 表示在冒泡階段執行(後面說明);也可以傳物件,例如 { once: true } 表示只執行一次後自動移除。
  • 同一個函式對同一個事件重複加入,只會留下一個。
  • 要移除時,removeEventListener 必須傳入同一個函式。直接寫在參數裡的匿名函式沒有名字可以參照,就移除不了。
  • 處理器用一般函式時,裡面的 this 就是掛上監聽器的元素(等於 e.currentTarget);用箭頭函式時 this 不是元素,請改用 e.currentTarget。

加入與移除處理器的規則

執行動畫在背景執行緒裡跑,沒有網頁與按鈕,所以用 EventTarget 物件代替按鈕(網頁上的元素都是 EventTarget),用 dispatchEvent 觸發事件。猜猜看:第一次 click 時 sayHi 會執行幾次?兩個內容一樣的箭頭函式呢?

互動練習:加入與移除處理器

「按我」的 HTML 標籤上寫了 onclick,JavaScript 又指定了一次 btn.onclick。先按「執行」,再到預覽裡點「按我」,看主控台出現哪一句。接著依任務修改 JavaScript。

想一想:事件處理器

1.

下面的程式執行後點一下按鈕,主控台會出現什麼?

btn.onclick = () => console.log('A');
btn.onclick = () => console.log('B');
2.

下面的程式會印出什麼?

const t = new EventTarget();
let n = 0;
function f() { n++; }
t.addEventListener('go', f);
t.addEventListener('go', f);
t.dispatchEvent(new Event('go'));
console.log(n);
3.

btn.addEventListener('click', () => alert('嗨')) 之後,怎麼樣才能移除這個處理器?

第 1 次作答

事件物件與常用事件

處理器的第一個參數就是事件物件,習慣命名為 e 或 event:

pad.addEventListener('click', function (e) {
  console.log(e.type);           // 'click'
  console.log(e.target);         // 實際被點到的元素
  console.log(e.clientX, e.clientY);   // 滑鼠相對於瀏覽器可視區域的座標
});
屬性 意思
e.type 事件名稱
e.target 事件真正發生的元素(例如被點到的最內層元素)
e.currentTarget 目前執行的這個監聽器掛在哪個元素上
e.clientX/e.clientY 滑鼠相對於瀏覽器可視區域的座標
e.offsetX/e.offsetY 滑鼠相對於目標元素左上角的座標
e.screenX/e.screenY 滑鼠相對於整個螢幕的座標
e.button 按下的是哪一個滑鼠按鍵(0 左鍵、1 中鍵、2 右鍵);e.buttons 可以同時表示多個按鍵
e.key 鍵盤事件按下的鍵,例如 'a'、'Enter'、'ArrowUp'

滑鼠事件:

事件 何時發生
mousedown/mouseup 按下/放開滑鼠按鍵
click 單擊;順序是 mousedown → mouseup → click
dblclick 雙擊;在兩次 mousedown → mouseup → click 之後才發生
mousemove 滑鼠在元素裡移動,移動時會連續發生很多次
mouseenter/mouseleave 滑鼠進入/離開元素;不會冒泡,移到子元素上也不會再發生
mouseover/mouseout 滑鼠進入/離開元素;會冒泡,在子元素之間移動時會發生很多次
contextmenu 按滑鼠右鍵

鍵盤與輸入事件:keydown(按下按鍵)、keyup(放開按鍵)、input(輸入框內容每次改變)、change(內容改變且離開輸入框,或選項改變)。 舊的 keypress 已經不建議使用,請改用 keydown。

互動練習:滑鼠、鍵盤與輸入事件

先按「執行」,到預覽裡點一下虛線框,主控台會依序出現 mousedown、mouseup、click。接著依任務加上處理器,每次改完都要按「執行」再到預覽裡操作。

事件流:捕獲、目標與冒泡

按鈕放在 div 裡,div 又放在 body 裡。點了按鈕,等於也點了外面的 div 與 body。 那麼,掛在這三個元素上的 click 處理器,誰先執行?瀏覽器把一次事件的傳遞分成三個階段:

  1. 捕獲階段(capturing phase):事件從最外層的 window、document 一路往內,傳到目標元素的外層
  2. 目標階段(at target):到達真正被點到的元素
  3. 冒泡階段(bubbling phase):事件再從目標一路往外傳回 document
捕獲 ↓  document → body → div →   [ button ]   ← 目標
冒泡 ↑  document ← body ← div ←   [ button ]

addEventListener 預設在冒泡階段執行,所以點按鈕時的順序是 button → div → body(由內往外)。 第三個參數傳 true,處理器就改在捕獲階段執行,會比內層的處理器先執行。

處理器裡呼叫 e.stopPropagation(),事件就不再往下一個元素傳遞。不過它也會讓外層需要知道這次點擊的程式(例如點空白處關閉選單)收不到事件,非必要不要用。

下面的小工具在三層元素上掛了真的監聽器,由瀏覽器實際派送事件,並依序亮起執行到的元素。

互動練習:觀察事件流

按鈕在 div#inner 裡,div#inner 又在 div#outer 裡,三者都掛了 click 監聽器,右邊顯示目前的程式碼。依序完成四個任務:先看預設的冒泡順序,再勾選「outer 用捕獲」與「inner 呼叫 e.stopPropagation()」,觀察順序怎麼改變;最後在下方的清單試試事件委派。

事件委派與 preventDefault

事件委派(event delegation):因為事件會冒泡,不必幫清單的每一項都掛監聽器,只要在外層的 <ul> 掛一個, 再用 e.target 判斷點到哪一項:

todo.addEventListener('click', (e) => {
  const li = e.target.closest('li');   // 從被點到的元素往外找最近的 <li>
  if (!li) return;                      // 點到的不是任何一項
  li.classList.toggle('done');
});

事件委派有兩個好處:監聽器只有一個;之後才新增的項目自動就有效果,不必再幫它掛監聽器。 這裡的 e.currentTarget 是 <ul>(監聽器掛的地方),e.target 是實際被點到的元素。用 closest('li') 是因為項目裡可能還有 <b> 這類子元素,e.target 不一定剛好是 <li>。

preventDefault:有些事件有瀏覽器的預設動作:點連結會前往網址、送出表單會重新載入頁面、點核取方塊會勾選。 在處理器裡呼叫 e.preventDefault() 就能取消預設動作,改由你的程式決定要做什麼。它和 stopPropagation 不同: preventDefault 取消的是預設動作,事件仍然會繼續冒泡。下一課的表單驗證會一直用到它。

互動練習:事件委派與取消預設動作

待辦清單已經有一個「新增項目」按鈕。請只在 #todo 上掛一個監聽器,讓點到的項目切換成完成(加上 class done);新增的項目不必另外掛監聽器也要有效。

想一想:事件物件與事件流

1.

<div id="box"><button>按我</button></div>,box 與按鈕都用 addEventListener('click', …) 掛了處理器(沒有第三個參數)。點按鈕時,哪一個先執行?

2.

用事件委派在 <ul> 上處理清單點擊,最主要的好處是什麼?

3.

要讓表單送出時不要重新載入頁面,處理器裡要呼叫哪一個方法?

4.

滑鼠在一個有很多子元素的 <div> 裡移動,哪一個事件只會在進入 div 時發生一次?

5.

btn.addEventListener('click', () => console.log(this === btn)),點按鈕時會印出什麼?

第 1 次作答

重點精華回顧

  • 事件是發生了什麼事,事件物件帶著這次事件的資訊,事件處理器(監聽器)是事件發生時要執行的函式。
  • 三種寫法:HTML 屬性 onclick="…"、元素的 onclick 屬性(一個事件只能有一個處理器)、addEventListener(可以有多個,建議使用)。
  • removeEventListener 要傳入同一個函式,匿名函式移除不了;{ once: true } 讓處理器只執行一次。
  • 事件物件常用的屬性:type、target、currentTarget、clientX/offsetX、key。
  • 滑鼠單擊的順序是 mousedown → mouseup → click;mouseenter/mouseleave 不會冒泡,mouseover/mouseout 會。輸入框每次改變用 input,改完離開才觸發的是 change。
  • 事件流分成捕獲、目標、冒泡三個階段;addEventListener 預設在冒泡階段執行,第三個參數 true 改成捕獲階段。
  • 事件委派:在外層掛一個監聽器,用 e.target.closest(…) 判斷點到哪一項;preventDefault 取消預設動作,stopPropagation 停止事件傳遞。
這一課完成了!
Next lesson:表單
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。