JavaScript 網頁事件處理
一、eventListener 與 callback function
目標:學習怎麼對事件做出反應。事件很多,例如:捲動(scroll), 按按鍵(keyDown)
eventListener (事件監聽)
首先要先選到元素,我們這邊選 block,接著 element.addEventListener(監聽事件名稱, 函數)。最後重新整理,跑一次 => 點 yo hello~ => 跳出 click
<!DOCTYPE html>
<html>
<head>
<meta charset="urf-8">
<title>i am title</title>
<link rel="stylesheet" href="./style.css"/>
</head>
<body>
<div id='block'>
yo
<a>hello~</a>
</div>
<script>
const element = document.querySelector('#block')
element.addEventListener('click',onClick)
function onClick() {
alert('click!')
}
</script>
</body>
</html>
整段代碼:我在 element 上新增 EventListener 他叫 click (我想監聽的事件叫做 click),當有 click 這個事件發生時,瀏覽器幫我觸發 onClick 這個函式。這個函式我們叫他 callback function。在這個 function 中可以做任何想做的事情。
大多數會偷懶直接用匿名函式寫,匿名函式和 callback function 一點關係都沒有,他只是沒有名稱。
<script>
const element = document.querySelector('#block')
element.addEventListener('click', function() {
alert('click!')
})
</script>
結論:
可以對一個元素進行監聽,監聽什麼樣的事件,一旦這個事件被觸發,就會執行到後面的 function。
二、詳細講解 callback function
callback function(回呼函式)
Recall:監聽事件要先建立事件監聽器,EventListener(),第一個傳要監聽的事件名稱,第二個傳一個 function。
當有人點按鈕時,幫我呼叫這個 function。也就是當有人觸發監聽事件,你再告訴我,我就不用一直等在那邊,我可以去做其他事情,一旦這個事件發生,就呼叫這個 function。
<script>
const element = document.querySelector('#block')
element.addEventListener('click', function() {
alert('click!')
})
</script>
為甚麼要這樣寫? 你不知道使用者甚麼時候要按按鈕,如果按照下列寫法,程式會一直卡在那邊等待事件觸發。
<script>
const element = document.querySelector('#block')
const event = element.addEventListener('click')
function onClick() {
alert('click!')
}
</script>
三、event(e) 是什麼碗糕?
callback function 可以拿到更多資訊。瀏覽器在乎叫 function 會拿進參數,參數可以隨意命名,通常命名 event 或 e。
<!DOCTYPE html>
<html>
<head>
<meta charset="urf-8">
<title>i am title</title>
<link rel="stylesheet" href="./style.css"/>
</head>
<body>
<div id='block'>
yo
<a>hello~</a>
</div>
<script>
const element = document.querySelector('#block')
element.addEventListener('click', function(e) {
console.log(e)
})
</script>
</body>
</html>
也可以是這樣的寫法
<script>
const element = document.querySelector('#block')
element.addEventListener('click', onClick)
function onClick(e) {
console.log(e)
}
</script>
e.target 表示我點了哪個元素。所以,可以用 e.target 取得我到底點了哪個元素。
<script>
const element = document.querySelector('#block')
element.addEventListener('click', function(e) {
console.log(e.target)
})
</script>
以瀏覽器的角度,當我點了這個 element,瀏覽器會幫我呼叫這個 function,然後 e 會帶一些資訊進去。所以後面的 e 是瀏覽器帶給我的資訊。
<body>
<div id='block'>
yo
<a>hello~</a>
</div>
<input/>
<script>
const element = document.querySelector('input')
element.addEventListener('keydown', function (e) {
console.log(e.key)
})
</script>
</body>
按按鈕切換背景顏色
<!DOCTYPE html>
<html>
<head>
<meta charset="urf-8">
<title>i am title</title>
<link rel="stylesheet" href="./style.css"/>
<style>
.active {
background: red;
}
</style>
</head>
<body>
<div id='block'>
yo
<a>hello~</a>
<button class='change-btn'>change </button>
</div>
<script>
const element = document.querySelector('.change-btn')
element.addEventListener('click', function (e) {
document.querySelector('body').classList.toggle('active')
})
</script>
</body>
</html>
e 是瀏覽器幫我傳過來的變數,裡面會有和我事件相關的資訊。
四、表單事件處理 onSubmit
可以在表單上加上 submit 這個事件,在表單送出以前就會觸發這個事件。可以對這個事件做一些處理,例如 : 表單不要送出,通常會用在表單驗證。
表單如果沒有指定,他預設的網頁會是同個網頁,method 會是 get。
<!DOCTYPE html>
<html>
<head>
<meta charset="urf-8">
<title>i am title</title>
<link rel="stylesheet" href="./style.css"/>
<style>
</style>
</head>
<body>
<form class='login-form' method="GET" action="">
<div>
username: <input name="username"/>
</div>
<div>
password: <input name="password" type="password"/>
</div>
<div>
password again: <input name="password2" type="password"/>
</div>
<input type="submit"/>
</form>
<script>
const element = document.querySelector('.login-form')
element.addEventListener('submit', function (e) {
alert("submit")
})
</script>
</body>
</html>
e.preventDefault() 表示阻止他預設的行為,表單的預設行為=submit,preventDefault() 是 e 裡面的一個函式。
所以現在就不送出了。
<script>
const element = document.querySelector('.login-form')
element.addEventListener('submit', function(e) {
e.preventDefault()
})
</script>
如何拿值,透過 input1.value
<!DOCTYPE html>
<html>
<head>
<meta charset="urf-8">
<title>i am title</title>
<link rel="stylesheet" href="./style.css"/>
<style>
</style>
</head>
<body>
<form class='login-form' method="GET" action="">
<div>
username: <input name="username"/>
</div>
<div>
password: <input name="password" type="password"/>
</div>
<div>
password again: <input name="password2" type="password"/>
</div>
<input type="submit"/>
</form>
<script>
const element = document.querySelector('.login-form')
element.addEventListener('submit', function(e) {
const input1 = document.querySelector('input[name=password]')
const input2 = document.querySelector('input[name=password2]')
if (input1.value !== input2.value) {
alert('密碼不同')
e.preventDefault()
}
})
</script>
</body>
</html>
五、阻止預設行為:preventDefault
阻止瀏覽器的預設行為 e.preventDefault,常見用法:超連結、表單驗證。
超連結的預設是按了之後點到某個地方,所以加了 e.preventDefault 不管怎麼點他都不會有反應。
<a href="https://www.google.com/">link</a>
</form>
<script>
const element = document.querySelector('a')
element.addEventListener('click', function(e) {
e.preventDefault()
})
</script>
這樣寫,表示我沒辦法打 e 這個字。
<body>
<form class='login-form' method="GET" action="">
<div>
username: <input name="username"/>
</div>
<div>
password: <input name="password" type="password"/>
</div>
<div>
password again: <input name="password2" type="password"/>
</div>
<input type="submit"/>
</form>
<script>
const element = document.querySelector('input[name=username]')
element.addEventListener('keypress', function(e) {
if (e.key === 'e') {
e.preventDefault()
}
})
</script>
</body>
六、事件傳遞機制
甚麼是事件傳遞機制
前言:
因為他們是有層層疊疊的關係,所以點綠色區塊,也會觸發到紅色區塊的東西。所以會先從點到最近的那個開始,然後慢慢擴散出去,像漣漪一樣。這個東西就是瀏覽器的事件傳遞機制。
<!DOCTYPE html>
<html>
<head>
<meta charset="urf-8">
<title>i am title</title>
<link rel="stylesheet" href="./style.css"/>
<style>
.outer {
width: 500px;
height: 200px;
background: red;
}
.inner {
width: 300px;
height: 100px;
background: green;
}
</style>
</head>
<body>
<div class='outer'>
<div class='inner'>
<button class='btn'>click me </button>
</div>
</div>
<script>
addEvent('.outer')
addEvent('.inner')
addEvent('.btn')
function addEvent(className) {
document.querySelector(className)
.addEventListener('click', function() {
console.log(className)
})
}
</script>
</body>
</html>
事件傳遞機制詳解:捕獲與冒泡
先捕獲再冒泡。
如何將 eventListener 掛在不同的階段 ? A:.addEventListener() 放第三個參數-布林,true:捕獲階段、false:冒泡階段(預設為 flase)。
點 click me => 發現先捕獲,再冒泡。
.outer 捕獲
.inner 捕獲
.btn 捕獲
.btn 冒泡
.inner 冒泡
.outer 冒泡
<body>
<div class='outer'>
<div class='inner'>
<button class='btn'>click me </button>
</div>
</div>
<script>
addEvent('.outer')
addEvent('.inner')
addEvent('.btn')
function addEvent(className) {
document.querySelector(className)
.addEventListener('click', function() {
console.log(className, '捕獲')
},true)
document.querySelector(className)
.addEventListener('click', function() {
console.log(className, '冒泡')
},false)
}
</script>
</body>
target phase 會依據你先放哪個 EventListener 而是哪個 EventListener。
ex:我們在觸發 clickme 他是處於 target phase
.outer 捕獲
.inner 捕獲
.btn 冒泡
.btn 捕獲
.inner 冒泡
.outer 冒泡
<script>
addEvent('.outer')
addEvent('.inner')
addEvent('.btn')
function addEvent(className) {
document.querySelector(className)
.addEventListener('click', function() {
console.log(className, '冒泡')
},false)
document.querySelector(className)
.addEventListener('click', function() {
console.log(className, '捕獲')
},true)
}
</script>
放一個監聽在捕獲階段,就可以記錄到所有按鈕的事件。
<body>
<div class='outer'>
<div class='inner'>
<button class='btn'>click me </button>
</div>
</div>
<script>
addEvent('.outer')
addEvent('.inner')
addEvent('.btn')
window.addEventListener('click', function(e) {
console.log(e)
}, true)
function addEvent(className) {
document.querySelector(className)
.addEventListener('click', function() {
console.log(className, '冒泡')
},false)
}
</script>
</body>
超連結點下去會完全沒有用,一旦 call 了 e.preventDefault(),這個 e.preventDefault() 會沿路傳下去。所以儘管我們是在這邊加上的,傳到 a 還是會有效果,因此整個頁面的表單送出、超連結都不會有效果。
<body>
<div class='outer'>
<div class='inner'>
<a href='./test'>click</a>
<button class='btn'>click me </button>
</div>
</div>
<script>
addEvent('.outer')
addEvent('.inner')
addEvent('.btn')
window.addEventListener('click', function(e) {
e.preventDefault()
}, true)
function addEvent(className) {
document.querySelector(className)
.addEventListener('click', function() {
console.log(className, '冒泡')
},false)
}
</script>
</body>
七、別向上級回報:stopPropagation
冒泡的過程就像逐級向上回報
當我們點按鈕時,只會觸發按鈕的事件;點綠色的地方,綠色的仍會傳遞。
stopPropagation() 會阻止這個事件繼續傳遞。
<!DOCTYPE html>
<html>
<head>
<meta charset="urf-8">
<title>i am title</title>
<link rel="stylesheet" href="./style.css"/>
<style>
.outer {
width: 500px;
height: 200px;
background: red;
}
.inner {
width: 300px;
height: 100px;
background: green;
}
</style>
</head>
<body>
<div class='outer'>
<div class='inner'>
<a href='./test'>click</a>
<button class='btn'>click me </button>
</div>
</div>
<script>
addEvent('.outer')
addEvent('.inner')
function addEvent(className) {
document.querySelector(className)
.addEventListener('click', function() {
console.log(className, '冒泡')
})
}
document.querySelector('.btn')
.addEventListener('click', function(e) {
e.stopPropagation()
console.log('.btn 冒泡')
})
</script>
</body>
</html>
發現,不管怎麼點都不會有 log,連剛剛的 EventListener 都不會觸發。這是因為你在 windows 這個階段就已經阻止他傳下去,所以下面的東西都不會收到事件。
<body>
<div class='outer'>
<div class='inner'>
<a href='./test'>click</a>
<button class='btn'>click me </button>
</div>
</div>
<script>
addEvent('.outer')
addEvent('.inner')
window.addEventListener('click', function(e) {
e.stopPropagation()
},true)
function addEvent(className) {
document.querySelector(className)
.addEventListener('click', function() {
console.log(className, '冒泡')
})
}
document.querySelector('.btn')
.addEventListener('click', function(e) {
e.stopPropagation()
console.log('.btn 冒泡')
})
</script>
</body>
一個按鈕可以加兩個 eventListener() 是沒有問題。兩個 eventListener() 都會被觸發。
<script>
addEvent('.outer')
addEvent('.inner')
function addEvent(className) {
document.querySelector(className)
.addEventListener('click', function() {
console.log(className, '冒泡')
})
}
document.querySelector('.btn')
.addEventListener('click', function(e) {
console.log('.btn click 1')
})
document.querySelector('.btn')
.addEventListener('click', function(e) {
console.log('.btn click 2')
})
</script>
加上 e.stopPropagation(),阻止冒泡,但兩個事件還是會被觸發。因為這兩個事件都在 target phase,所以雖然阻止事件傳遞原本的還是會被觸發。
<body>
<div class='outer'>
<div class='inner'>
<a href='./test'>click</a>
<button class='btn'>click me </button>
</div>
</div>
<script>
addEvent('.outer')
addEvent('.inner')
function addEvent(className) {
document.querySelector(className)
.addEventListener('click', function() {
console.log(className, '冒泡')
})
}
document.querySelector('.btn')
.addEventListener('click', function(e) {
e.stopPropagation()
console.log('.btn click 1')
})
document.querySelector('.btn')
.addEventListener('click', function(e) {
e.stopPropagation()
console.log('.btn click 2')
})
</script>
</body>
加上 e.stopImmediatePropagation() 就會阻止其他任何的 EventListener => 只會觸發第一個。會當前立刻阻止所有事件傳遞。
<script>
addEvent('.outer')
addEvent('.inner')
function addEvent(className) {
document.querySelector(className)
.addEventListener('click', function() {
console.log(className, '冒泡')
})
}
document.querySelector('.btn')
.addEventListener('click', function(e) {
e.stopImmediatePropagation()
console.log('.btn click 1')
})
document.querySelector('.btn')
.addEventListener('click', function(e) {
e.stopPropagation()
console.log('.btn click 2')
})
</script>
八、新手 100% 會搞錯的事件機制問題
首先,我們有兩個按鈕,class 都叫 btn,
<!DOCTYPE html>
<html>
<head>
<meta charset="urf-8">
<title>i am title</title>
<link rel="stylesheet" href="./style.css"/>
<style>
</style>
</head>
<body>
<div class='outer'>
<button class='btn'>1</button>
<button class='btn'>2</button>
</div>
<script>
document.querySelector('.btn')
.addEventListener('click',function() {
})
</script>
</body>
</html>
現在幫 .btn 加 EventListener,點了就是 1 => 點了 btn1 出現 1,但點了 btn2 沒反應。這是因為 document.querySelector 只會回傳第一個元素。也就是說,我們只有幫第一個元素加 EventListener。
document.querySelector('.btn')
.addEventListener('click',function() {
alert(1)
})
如果想幫每一個都加,使用 querySelectorAll => TypeError: document.querySelectorAll(...).addEventListener is not a function
這是因為 querySelectorAll 回傳的是一個 list。
document.querySelectorAll('.btn')
.addEventListener('click',function() {
alert(1)
})
修改成以下形式:發現不管點哪個 btn 都是 6
=> 這是因為 scope
<body>
<div class='outer'>
<button class='btn'>1</button>
<button class='btn'>2</button>
<button class='btn'>3</button>
<button class='btn'>4</button>
<button class='btn'>5</button>
</div>
<script>
const elements = document.querySelectorAll('.btn')
for (var i=0; i<elements.length; i++) {
elements[i].addEventListener('click',function() {
alert(i+1)
})
}
</script>
</body>
這個 function 是在我點擊那刻觸發,點擊那刻 i 的值是 elements.length ,所以迴圈跑完時 i = 5,所以當我點擊時 alert(i+1) = 6
<script>
const elements = document.querySelectorAll('.btn')
for (var i=0; i<elements.length; i++) {
elements[i].addEventListener('click',function() {
alert(i+1)
})
}
alert(i)
</script>
所以可以如何修正 ? (1) 用 let (2) 新增 data-value,以屬性的方式拿取。
<div class='outer'>
<button class='btn' data-value='1'>1</button>
<button class='btn' data-value='2'>2</button>
<button class='btn' data-value='3'>3</button>
<button class='btn' data-value='4'>4</button>
<button class='btn' data-value='5'>5</button>
</div>
<script>
const elements = document.querySelectorAll('.btn')
for (var i=0; i<elements.length; i++) {
elements[i].addEventListener('click',function(e) {
console.log(e.target.getAttribute('data-value'))
})
}
</script>
現在,我們想新增按鈕。
<body>
<div class='outer'>
<button class='add-btn'>add</button>
<button class='btn' data-value='1'>1</button>
<button class='btn' data-value='2'>2</button>
</div>
<script>
let num=3
const elements = document.querySelectorAll('.btn')
for (var i=0; i<elements.length; i++) {
elements[i].addEventListener('click',function(e) {
alert(e.target.getAttribute('data-value'))
})
}
document.querySelector('.add-btn').addEventListener('click', function() {
const btn = document.createElement('button')
btn.setAttribute('data-value', num)
btn.innerText = num
num++
document.querySelector('.outer').appendChild(btn)
})
</script>
</body>
為甚麼點新增的按鈕,沒有反應 ?
當我在跑 querySelectorAll => 他的 element 也只有 1, 2 兩個按鈕,動態新增後才有新的按鈕,但動態新增的部分並未幫他們加 EventListener。所以他點了就不會有反應。
整理:
- 點擊產生事件,會從上傳到下,再從下傳到上。事件機制是不管如何都會發生。不管有沒有加監聽器,事件都會這樣傳。加了監聽器表示在傳遞時,Listener 就可以監聽到這個事件,拿到這個事件,然後我就可以決定怎麼處理。更詳細地說,事件不管怎樣都會傳,我加上監聽器去攔截事件,拿到事件決定對事件如何處理,再繼續傳遞下去或不傳遞。
- e.stopPropagation() 阻止事件傳遞,後面的連結會斷掉。所以我後續的 node 就收不到這個事件。
- 第三個參數,true/ false 是決定事件要加在哪個 phase
- 點到元素本身是 target phase,這時觸發的順序依照先捕獲後冒泡依序傳遞。
九、欸等等幫我拿餐點:event delegation
如果有一百個按鈕,就要加一百個 EventListener ? 有一千個按鈕就要加一千個 EventListener ? 將性質相似的每個元素加上其專屬的 EventListener 這樣顯然很沒效率。
所以怎麼辦呢 ?
我們可以利用事件冒泡的性質,每一個 click event 都會冒泡到 outer 去。所以在 outer 上加 EventListener 就好了。就可以處理下面所有的 btn,動態新增的也可以。
這就叫做 event delegation 事件代理。白話的說,一群人去麥當勞點餐,不可能所有人都在下面等,有一兩個在樓下幫忙取餐,其他人先占位子。那個人就負責拿所有人的餐點,現在這個 outer 就是那個幫大家拿餐點的人。
本來事件就會一直冒泡。
<body>
<div class='outer'>
<button class='add-btn'>add</button>
<button class='btn' data-value='1'>1</button>
<button class='btn' data-value='2'>2</button>
</div>
<script>
let num=3
document.querySelector('.add-btn').addEventListener('click', function() {
const btn = document.createElement('button')
btn.setAttribute('data-value', num)
btn.innerText = num
btn.classList.add('btn')
num++
document.querySelector('.outer').appendChild(btn)
})
document.querySelector('.outer').addEventListener('click', function(e) {
if (e.target.classList.contains('btn')) {
alert(e.target.getAttribute('data-value'))
}
})
</script>
</body>
事件代理機制的優點:
(1) 有效率,不用浪費資源建立無數個做相同事件的 function。只要用一個 EventListener 就可以管理處理這些事情。
(2) 處理動態新增的信息。透過冒泡機制,讓底下就算是新增的東西,一樣可以接到他的事件。