Day28 | JS Event 事件

❒ 本篇會介紹到的部分

addEventListener 監聽、Event Bubbling 事件冒泡、Event Capturing 事件捕捉、stopPropagation 終止冒泡事件、preventDefault 取消預設、e.target 搭配 nodeName 與 keyCode 、表單中 blur, focus, click 事件、keydown 事件、mousemove, mouseleave 事件、網頁座標 ( screen、page、client )。

❒ 各種事件綁定的差異

➊ 不推薦在 HTML 中執行 JS,此為很老式的寫法

1
<input type="button" onclick="alert('Hello')" value="點擊" class='btn'>

在 HTML 中執行 JS ( 以 onclick 為例 ),不推薦的原因 :

  • 在 JS 中可以設定在某些條件下才做綁定執行 onclick ,不像 HTML 中 onclick 會一直執行。
  • 可能在 onclick 中被插入惡意程式碼。

➋ 監聽的推薦寫法

1
btn.addEventListener('click' , function(){ },false)  

❒ addEventListener - 事件監聽

結構

addEventListener( '參數1' ,參數2 ,參數3 );

  1. 參數1 : 選擇事件,例如 "click" 亦可換為其他事件。滑鼠的各種事件可參閱 w3school - HTML DOM Events
  2. 參數2 : function(e){} ,函式內可加入 e ,會回傳 event 事件。
  3. 參數3 : false
    1. true : 從外層往內層找指定元素。
    2. false( 預設 ) : 指定元素往外層找,預設為 false ,所以如果沒寫就會是預設狀態。

CodePen 範例

範例 1. 寫一個加法器,點選按鈕下方數字會做加總

出處 : JavaScript 工程師養成直播班 - 2021 春季班影音

CodePen 範例

為什麼 let count = 0; 不能放在 function(e){} 內 ?

函式內的大括號 {} 執行完會釋放記憶體,如果不希望值因函式運算完而消失,就需要放在外層,函式內運算完就會往外送依序做加總。

❒ 如何觀看 DOM 有註冊事件監聽

出處 : JavaScript 工程師養成直播班 - 2021 春季班影音

可搭配 chorme 開發者工具查看按鈕是否有註冊監聽事件

開發者工具截圖

  1. 於監聽的按鈕點選滑鼠右鍵 / 檢查
  2. 開發者工具 Elements / Event Listeners 可看到目前的監聽事件們。

❒ event 物件 - 告訴你當下元素資訊

DOM events 事件,例如滑鼠的各種事件可參閱 w3school - HTML DOM Events ,但現在不推薦在 HTML 使用 JS,因爲較老式用法有許多缺點下方會說到。

範例 1. 以按鈕為例,點擊按鈕跳出彈跳視窗 hello

CodePen 範例

範例 2. 點擊按鈕知道滑鼠的位置或出現此按鈕的詳細資訊 ( type ) 需透過甚麼方式呢 ?

CodePen 範例

  • 點擊按鈕知道滑鼠位置,應用在點擊某個 XY 軸位置,會出現資訊視窗。
  • MouseEvent 為滑鼠事件,會把當下點擊的那一瞬間的所有資訊都記錄在物件裡,可運用裡面的物件去做對應的事情,例如下方要讀取點擊按鈕的 X 軸座標位置 clientX,可從函式參數 e 去讀取裡面的屬性 clientX
  • 執行監聽時,會在你命名的函式中帶入一個參數 e ( 參數名可自訂 ),所以點擊按鈕後會把所有物件 ( MouseEvent 為物件 object ) 資訊紀錄到這個參數內。
    • 開啟開發者工具 Console 可見 MouseEvent ( MouseEvent 為物件 object ) 顯示按鈕的詳細資訊。
      ![截圖](https://ithelp.ithome.com.tw/upload/images/20221013/20119743agRsJZiia2.png)
      
    • W3school - HTML DOM MouseEvent 相關方法

❒ 綁定事件的語法差異

onclickaddEventListener( "click" , function(e){},false) 差異 ?

CodePen 範例

  • onclick 不能同時綁定兩個事件。如果綁定兩個事件以上,只會讀取最後一個。
  • addEventListener( "click", function(e){}, false); 可以同時綁定多個事件。

❒ Event Bubbling 事件冒泡、Event Capturing 事件捕捉的差異

addEventListener ()truefalse 差異?

  1. true ( Event Capturing 事件捕捉 )
    1. 從外層往內層找指定元素。
  2. false( Event Bubbling 事件汽泡 )
    1. 指定元素往外層找。
    2. 預設為 false ,所以如果沒寫就會是預設狀態。
    3. 較常使用 false ,點擊最近的東西來去觸發事件。

範例 1. 如果 .body.box 為 false,點選 box 區域時會先執行哪個呢 ?

CodePen 範例

  • false ( Event Bubbling 事件汽泡 ) : 指定元素往外找 。以範例來說,當下點的指定元素 .box 往外層找 .body → 先跳出 box 彈跳視窗再出現 body 彈跳視窗
  • true ( Event Capturing 事件捕捉 ) : 從最外層往內找到指定元素。以範例來說,外層 .body 往內找到指定元素 .box → 先跳出 body 彈跳視窗再出現 box 彈跳視窗
  • 所以較常使用 false ,點擊最近的東西來去觸發事件。
    示意圖

範例 2. 承範例 1.,如果 body 為 true , .box 為 false,點選 box 區域時會先執行哪個呢 ?

CodePen 範例

  • .box 區域於 body 範圍內,而 bodyaddEventListener 是設定 true ( 從外層找到指定元素 ),所以會先跳 body 再跳 .box

❒ stopPropagation - 中止冒泡事件

防止監聽的元素互相衝突

當點選到重疊的元素時,只希望執行一個元素。可使用 stopPropagation 當指定元素和上層元素有重疊時,會阻止指定元素往外層找。

範例 1

CodePen 範例

  • 如果未加上 stopPropagation,點擊 .box 會彈出 boxbody 視窗 ( 事件冒泡由指定元素往外執行 )。
  • .box 加上 stopPropagation 後終止冒泡事件,點擊 .box 後就只會彈出 box 的 alert 視窗。
  • 如果外層沒有另外的事件會被觸發,那就不需要 stopPropagation 囉!

❒ preventDefault 取消預設觸發行為

常使用的情境:

  1. a 連結。
  2. submit 按鈕 ,先透過 JS 去查詢表單是否有誤,再透過 post 傳送。而非默認的 submit 後就直接傳送到後端做驗證。

範例 1. 使用 preventDefault,取消 a 連結觸發

CodePen 範例

  • 使用 preventDefault 取消預設觸發行為後,點擊 a 連結就不會轉跳到 google 頁面。

❒ e.target - 了解目前所在元素位置

e.target 可知道目前點擊的 DOM 範圍。

範例 1

CodePen 範例

  • 監聽最外層 ul 標籤,分別點擊 li 與 a 標籤,可於開發者工具看見這兩個 DOM 的範圍。
  • 另外 console.log (e); 部分,開啟開發者工具也可以看到點擊元素的目前所在位置 target : li
    開發者工具中 target : li

❒ nodeName 是什麼

nodeName 為 DOM 元素的節點,可搭配 e.target 使用。

❒ e.target 搭配 nodeName 節點,抓到你預期要做的事情

出處 : JavaScript 工程師養成直播班 - 2021 春季班影音

範例 1

CodePen 範例

1
2
3
4
5
6
7
<input type="button" value="點擊" class='btn'>
<ul class="list">
<li>標題 1</li>
<li>標題 2
<input type="button" class="btn" value="按鈕">
</li>
</ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const btn = document.querySelector('.btn');
const list = document.querySelector('.list');

btn.addEventListener('click', function (e) {
console.log(e.target.innerHTML); //點擊 input 因為裡面沒有 HTML 結構,所以會顯示空值。
console.log(e.target.nodeName); //INPUT
})

// 下方為點擊 li 標題2 的按鈕(下圖一)
list.addEventListener('click', function (e) {
console.log(e.target.innerHTML); //點擊 input 因為裡面沒有 HTML 結構,所以會顯示空值。
console.log(e.target.nodeName); //INPUT
})

//點擊第二個 li 的標題2文字 (下圖二)
list.addEventListener('click', function (e) {
console.log(e.target.innerHTML); //顯示 → 標題 與 input 結構
console.log(e.target.nodeName); //LI
})

點擊第二個 li 標題內的按鈕 button ( 圖一 )
點擊第二個 li 的標題文字 ( 圖二 )

範例 2. 使用 nodeName 判斷是否有點擊到按鈕,正確點擊到顯示”你目前點到按鈕了“

1
2
3
4
5
6
<ul class="list">
<li>標題</li>
<li>標題
<input type="button" class="btn" value="按鈕">
</li>
</ul>

CodePen 範例

  • 先宣告變數,綁 .list 這個 DOM。所以 ul.list > li *2 以內的範圍都會被綁定。
  • ul.list > li *2 範圍內觸發 click,就會觸發函式。
  • e.target.nodeName 會顯示 DOM 元素點擊到的節點為 INPUT 標籤。
  • 使用判斷式判斷 e.target.nodeName 與點擊到的DOM 節點是否相同,是的話就觸發大括號 {} 內的程式碼。

❒ change - 表單內容,更動內容時觸發

範例 1. 選擇區域顯示每個區域的農夫名

表單示意圖

1
2
3
4
5
6
<select id="areaId">
<option selected disabled>請選取區域</option>
<option value="苓雅區">苓雅區</option>
<option value="前鎮區">前鎮區</option>
</select>
<ul class="list"></ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const country = [
{
farmer: '查理',
place: '苓雅區'
},
{
farmer: '布朗',
place: '前鎮區'
},
{
farmer: '派翠克',
place: '苓雅區'
},
]

CodePen 範例

  • 選單在 change 時會觸發後方函式。
  • 透過 forEach 撈出變數 country 內的陣列資料,並使用判斷式篩選,讓選單的值與 forEach 撈出的 place 相同時,列出農夫名字。
    • e.target.value 可撈出選單中文字欄位內的值。
  • 因為要撈出某個區域的農夫,所以先寫一個空字串 let str = '';,等跑完 forEach 再帶到 li 內 。
    • innerHTML 特性 → 新帶入的值會把舊的值蓋過,為了防止資料被蓋掉寫一個空字串 let str = ''; ,來做累加字串。
    • 最後再把 if 篩選完的資料賦予 list.innerHTML

備註

list.innerHTML = str; 寫在 forEach 內與外雖然網頁皆顯示無誤,但對運行程式碼的效能考量不理想。

  • 寫在 forEach 內運行的次數會和陣列長度一樣。
  • 寫在 forEach 外只會運行一次。

❒ 表單中 blur 與 focus

  • focus 所在焦點
  • blur 離開焦點
  • 當元素聚焦時,會觸發 focus 事件,當元素失去焦點時,會觸發 blur 事件。

以表單為例,滑鼠點擊表單要輸入數字 ( 點擊了表單即觸發 focus 事件 ),但沒輸入就離開 ( 再點擊表單外的區域 ),就會觸發 blur 事件。

範例 1. 使用計算機計算出餐點金額,並搭配 blur 事件做欄位防呆機制

未填寫表格就點選計算中按鈕,會出現 NaN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<h2>六角西餐廳 - 顧客點餐系統</h2>
<ul>
<li>
服務生 : Hello,請問你想要點甚麼 ?
</li>
<li>
顧客 : 給我
<input type="text" class="hamburgerId">
個漢堡,再
<input type="text" class="cokeId">
杯可樂。
</li>
<li>
服務生:
<button class="startCountId">計算</button>
,好的,總計是
<em class="totalId"></em>

</li>
</ul>
1
2
3
4
5
6
7
8
9
//歸戶
const hamNum = document.querySelector('.hamburgerId');
const cokeNum = document.querySelector('.cokeId');
const startCount = document.querySelector('.startCountId');
const total = document.querySelector('.totalId');

// 漢堡與可樂價錢
const hamPrice = 70;
const cokePrice = 50;

CodePen 範例

  • 用到 isNaN 判斷是否為數字
    • JavaScript isNaN 函數用來判斷參數是否為特殊的非數字值 NaN,如果是就回傳 true,不是則回傳 false,要注意的是 isNaN 函數會將參數轉換成數字 ( number ),如果轉換成功就會回傳 true,失敗則回傳 false
    • 相關資訊:Wibibi - JavaScript isNaN 函數、MDN - isNaN()

❒ return 和 return false 的差異

Q1. isNaN 中使用到 return false ,而 returnreturn false 的差異為何呢 ? ( 助教回覆 )

Ans :
return 在函式內使用,會回傳一個值,並終止函式往下執行。
return false 通常用來阻止提交表單或是繼續執行下面的程式碼。簡單來說就是阻止執行預設的行為。

.

Q2. return false;

return false; 下方 1 與 2 的事情它都會做:

  1. 他會做 event.preventDefault();
  2. 他會做 event.stopPropagation();
  3. 停止 callback function 的執行並且立即 return 回來。

出處 : event.preventDefault()跟return false的差別是?

❒ keyCode - 點擊鍵盤,射發火箭

鍵盤式移動的遊戲,或互動式網站可使 keyCode

範例 1. 如何查詢鍵盤上每個按鍵的 keyCode

1
2
3
4
const body = document.body;
body.addEventListener('keydown', (e) => {
console.log(e.keyCode);
})
  • 監聽使用 keydown 指按下鍵盤的那個剎那,任何的鍵盤按鍵按下都可以取得對應的鍵盤代碼,也就是所謂的 keyCode
  • 輸入下面程式碼,開啟開發人員工具,於網頁上輸入鍵盤上任一按鍵,會回傳 keyCode
    開發者工具中回傳的 keyCode

範例 2. 按下鍵盤任一按鍵,顯示 keyCode 在網頁上

CodePen 範例

  • 監聽整個網頁,當在這個頁面執行 keydown 時 (按下鍵盤任一鍵),會觸發函式。
  • 使用 renderCode()keyCode 渲染在畫面上。

範例 3. 火箭遊戲練習 : 按下數字分別發射火箭,按 1 發射第一個火箭,按 2 發射第二個火箭,按 3 發射第三個火箭

範例示意圖

CodePen 範例

  • keydown 查出按鍵 1、2、3 的 keyCode
  • 監聽整個網頁,當有人按下鍵盤 ,執行函式內的程式碼。
  • 因知道明確數字 ( 上方以查出鍵盤 1、2、3 的 keyCode ),所以可以使用 switch 做控制判斷。

關於 Document.body

  • Document 介面代表所有在瀏覽器中載入的網頁,也是作為網頁內容 DOM Tree(包含 <body><table> 與其它的元素 )的進入點。
  • Document.body 主要是「 回傳目前文件的 <body> 節點,如元素不存在則回傳 null 」,因此只有 body 能使用。

❒ mousemove 當滑鼠滑入指定內容時觸發

mousemove 滑鼠移動到指定元素後觸發。

mouseleave 滑鼠離開指定元素後觸發。

範例 1. 滑鼠滑入方形區塊內,變圓形

CodePen 範例

  • 使用 mousemove 搭配 setAttribute 讓滑鼠進入 box 區域後,讓 box 由方形變圓形。

範例 2. 延續範例1. 滑鼠離開後再由圓形變回方形

CodePen 範例

  • 使用 mousemove 搭配 setAttribute 讓滑鼠進入 box 區域後,讓 box 由方形變圓形。
  • 為了讓整個過程順暢點,再加入 mouseleave 讓滑鼠離開 box 區域後再變回方形。
    • 使用 classList.remove 刪除圓形 boxRadius,再使用 classList.add 新增回原本方形 box
1
2
3
4
5
// 刪除單個
document.getElementById("myDIV").classList.remove("mystyle");

// 刪除多個
document.getElementById("myDIV").classList.remove("mystyle", "anotherClass", "thirdClass");

範例 3. 使用 animation 與 JS 的 mousemove 做小遊戲

  • animation 屬性的應用可參閱 「 Day8 | Animation、Transition、Transform 動畫效果 」
  • 範例

關於 classList

DOM 裡每個節點都有一個 classList 物件,可以使用來新增、刪除、修改節點上的 CSS 。

  • 刪除 remove
  • 新增 add
  • 切換 toggle

❒ 網頁座標 - 了解 screen、page、client 箇中差異

可先開啟 CodePen 範例 搭配下方解說

screen

螢幕 ( 手機、平板 、電腦等裝置 ) 解析度的座標點

page

  • 以整個網頁寬高為主
  • 如果頁面太長出現卷軸,Y 軸也會跟著持續增加。
    • 範例中 當右邊卷軸往下滑動,pageY 座標點數字會持續增加,而 clientY 只會顯是當下瀏覽器高度的座標點。

client

  • 瀏覽器窗口的座標點
  • 如果瀏覽器頁面太長出現卷軸,Y軸也只會出現當下視窗高度的座標點
    • 範例中 當右邊卷軸往下滑動,pageY 座標點數字會持續增加,而 clientY 只會顯是當下瀏覽器高度的座標點。

範例 1

1
2
3
4
5
6
7
8
function showPosition(e){
screenX.textContent = e.screenX;
screenY.textContent = e.screenY;
pageX.textContent = e.pageX;
pageY.textContent = e.pageY;
clientX.textContent = e.clientX;
clientY.textContent = e.clientY;
}

CodePen 範例

解說

  • 程式碼中的 eshowPosition 函式中的參數,可顯示出 showPosition 函式的所有資訊。
    console.log(e)可顯示函示的所有資訊
  • event 內有很多方法,如下示意圖 console.log(e) 顯示的函式資訊。
    • 以其中的方法之一的 screenX 為例 → e.screenX ( 亦可換 screenXscreenYtarget .. 等 ) 搭配 textContent ,可把 screen X 座標資訊顯示在網頁上。

範例 2. 網頁座標 ( 圖案取代鼠標 - clientXY )

範例示意圖

CodePen 範例

CSS

  • 避免原來的滑鼠指標 & 圖案取代的滑鼠指標 同時出現,於 body CSS 中使用 cursor:none;
  • .mouseImg 使用 fixed 可以不會因為旁邊的捲軸滾動而影響到位置 → 如圖 ,但使用 absolute 就會因為卷軸滾動而讓 icon 移動到上方 如圖

JS

如何讓圖案取代的鼠標 & 原來的鼠標同步 ?

  • 透過 DOM API 來操作 HTML 元素的 CSS 樣式 (style)。
  • 因 client XY 會顯示滑鼠於瀏覽器窗口的座標點,所以使用它來運算。
    • 讓滑鼠運算窗口的位置

❒ 事件監聽優化篇 - 從父元素來監聽子元素內容

範例 1. 父元素內有很多子元素,querySelector() 只會讓第一個子元素會獲得監聽

CodePen 範例

  • 滑鼠點選湯姆, console.log 出現湯姆,但點選捷克時,並不會顯示捷克。因為 querySelector() 的特性只會抓第一筆資料做更新

範例 2. 透過 noedName 確定點擊的東西是不是我要的節點

點選湯姆區域,Console 顯示節點為 LI

CodePen 範例

  • 監聽 ul 區域,搭配 e.target.nodeName 點擊區域內項目可確定是否有點擊到需要的節點。

範例 3. 不想點擊到 ul 範圍出現所有 li 內容,希望點擊 li 才觸發事件該怎麼做

CodePen 範例

使用情境:

  • ul>li 為例:
    • 如果子元素有很多個,希望每個都獲得監聽,可直接監聽父元素 ul,每個子元素 li 皆可獲得監聽。
    • 好處為可以透過 event  顯示出父元素內的所有資訊,但缺點就會像下方示意圖,點擊了 ul 範圍出現所有 li 內容。
      ![上述做法,單點 li 不會有問題,但點選 ul .list 範圍處會出現所有 li 名稱](https://ithelp.ithome.com.tw/upload/images/20221013/20119743PEfU5MeMH5.png)
      
    • 只想要點選 li ,Console 顯示 li 的值 ( 湯姆、捷克 ),點選其他元素 Console 皆不顯示,可加入判斷式 if。

解析:

  • 加入判斷式 if。
  • 透過 nodeName 方式查詢到 li 們的節點為 LI 把它帶入判斷式 if 中 → if( e.target.nodeName !== 'LI' ){return} 點擊到的節點不等於 li 回傳 return
    • retuen 可當回傳值外亦可當中斷點。 當中 retuen 也可不寫讓它是空值,這樣後方程式碼就會停止運作 ( 後方的console.log() 就不執行 ) 。
    • console.log(e.target.nodeName); 點選 li 內的湯姆與捷克, Console 出現的節點為 LI 所以判斷式內寫 !== 'LI'
    • 最後,除了 li 內的值會顯示外,點選其他元素 Console 皆不會顯示。

範例 2. 點選左方 box 內的文字,文字會顯示於右方 box 內

1
2
3
4
5
6
7
8
9
<div class="box">
<ul class="boxList">
<li>01. coco</li>
<li>02. coxima</li>
</ul>
</div>
<div class="box2">
<ul class="box2List"></ul>
</div>

CodePen 範例

  • 於全域設定一個空陣列 box2Data,點擊 .boxList>li 的文字可以 push 到空陣列中,再渲染到 .box2List 內。

資料來源