# 1-5 事件處理
事件是 JavaScript 的靈魂,一個具有高度互動性的網頁介面,對於各種事件的管理必定少不了。
在進入本篇之前,我們大多透過 v-model
指令來與 Vue 實體的資料溝通,但那畢竟只是語法糖的一種,
Vue.js 甚至是 JavaScript 能做到的遠遠不僅於此。
本篇的內容將帶領各位讀者理解, Vue.js 如何來操作與管理網頁的事件。
# 事件綁定 - v-on
有別於前面一篇介紹了一堆與模板相關的指令,在 Vue.js 與事件有關係的指令就只有一個 v-on
。
使用方式也很簡單,最常見的方式是 v-on:[事件名稱]="運算式"
,如:
const vm = Vue.createApp({
data () {
return {
count: 0
}
},
}).mount('#app');
2
3
4
5
6
7
<!-- 模板裡無需加上 this -->
<p>Count: {{ count }}</p>
<button v-on:click="count++">Plus</button>
2
3
或者也可以把 event handler function 寫在 methods 裡頭:
const vm = Vue.createApp({
data () {
return {
count: 0
}
},
methods: {
plus () {
this.count++;
}
}
}).mount('#app');
2
3
4
5
6
7
8
9
10
11
12
<p>Count: {{ count }}</p>
<button v-on:click="plus">Plus</button>
2
像這樣,我們將事件的處理器 (event handler) 事先在 methods
屬性宣告,然後透過 v-on
指令來綁定至對應的事件。
小提醒
與 v-bind
一樣, v-on
指令也提供了簡寫表示。
除了完整的 v-on:事件名稱="運算式"
寫法外,也可以簡寫成 @事件名稱="運算式"
,如:
<button v-on:click="alert('Hello!')">Click</button>
<button @click="alert('Hello!')">Click</button>
2
以上兩種寫法在執行時皆是同樣的結果。
# v-on
與 event 物件
在 JavaScript 裡,當監聽的事件發生時,EventListener
會去建立一個「事件物件」 (Event Object),
裡面包含了所有與這個事件有關的屬性,像是各種事件通用的屬性與方法、觸發事件的元素等等資訊。
像是大家很常用的 preventDefault()
、 stopPropagation()
等都是由 event
物件所提供的方法。
那麼我們要如何在 v-on
與 mehtods
取得這個 event
物件呢?
事實上, v-on
觸發事件時,如果沒有指定參數,預設就會將 event
物件當作參數傳入:
<button v-on:click="plus">Plus</button>
methods: {
plus (event) {
// "BUTTON"
console.log(event.target.tagName);
}
}
2
3
4
5
6
像這樣,程式在執行 plus
的時候,就可以取得預設傳入的 event
物件。
如果在 methods
需要傳入參數的情況,則可以在模板中指定傳入 $event
來指定 event 物件:
<p><input v-model.number="amount"></p>
<p>Count: {{ count }}</p>
<button v-on:click="plus(amount, $event)">Plus</button>
2
3
4
data () {
return {
amount: 0,
count: 0
}
},
methods: {
plus (amount, event) {
// "BUTTON"
console.log(event.target.tagName);
this.count += amount;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
像這樣在 methods
就可以取得 event
物件了。
# v-on
與修飾子
與 v-model
一樣, Vue.js 針對事件的 v-on
指令也提供了修飾子。
# 事件通用型修飾子
事件通用型修飾子主要有以下這些:
.stop
.prevent
.capture
.self
.once
.passive
# .stop
.stop
的作用就如同大家熟知的 event.stopPropagation()
,用來阻擋事件冒泡。
<div class="outer" @click="alert('Outer')">
<span>Outer</span>
<div class="inner" @click.stop="alert('Inner')">Inner</div>
</div>
2
3
4
像這樣,在 inner
區塊加上 @click.stop
, click
事件就不會向外層傳遞。
# .prevent
.prevent
的作用就如同大家熟知的 event.preventDefault()
,用來阻擋元素的預設行為。
<a href="#" class="btn" @click.prevent="alert('Hello!')">click me!</a>
當然也可以與 .stop
合用:
<a href="#" class="btn" @click.stop.prevent="alert('Hello!')">click me!</a>
# .capture
.capture
的作用,用來指定事件以捕獲 (capturing) 的形式來觸發:
<div class="outer" @click.capture="alert('Outer')">
<span>Outer</span>
<div class="inner" @click="alert('Inner')">Inner</div>
</div>
2
3
4
像這樣,我們在 outer
的 click
事件指定 .capture
修飾子,
在沒有加上 .capture
的情況點擊 inner
,會先觸發 inner
的 click
事件再觸發 outer
。
而當 outer
的 click
事件加上 .capture
之後順序則相反,點擊 inner
時會先印出 outer
再來才是 inner
。
小提醒
如果不熟悉 JavaScript 事件冒泡與捕獲運作機制的朋友,可參考拙作 重新認識 JavaScript: Day 14 事件機制的原理 (opens new window) 會有更詳細的說明。
# .self
.self
的作用,只會觸發元素自己的事件行為,由子層元素傳遞來的事件則不會觸發。
舉個很常見的燈箱範例來說,當我們希望在燈箱開啟後,點擊燈箱外遮罩可以自動關閉燈箱,也許我們可以這樣做:
<div class="modal-mask" :style="modalStyle">
<div class="modal-container" @click="isShow = false">
<div class="modal-body">Hello!</div>
</div>
</div>
<button @click="isShow = true">Click Me</button>
2
3
4
5
6
7
乍看之下似乎成功,但由於 modal-body
的內容區塊也在 modal-container
之下,所以當內容被點擊時,燈箱也會被關閉。
此時,若是在 modal-container
的 click
事件加上 .self
修飾子,就可以排除這樣的問題:
<div class="modal-mask" :style="modalStyle">
<div class="modal-container" @click.self="isShow = false">
<div class="modal-body">Hello!</div>
</div>
</div>
<button @click="isShow = true">Click Me</button>
2
3
4
5
6
7
小提醒
一個指令可以同時加入多個修飾子,而修飾子的順序會影響執行的結果。
如 v-on:click.prevent.self
會先執行 prevent
的動作,阻擋所有的點擊行為;
而 v-on:click.self.prevent
會先執行 self
,只會阻擋該元素自己的點擊行為。
# .once
.once
修飾子的作用,讓指定的事件只會觸發一次。
<button @click.once="plus">Plus Once</button>
# .passive
.passive
等同於 addEventListener
的 passive
屬性,
用途是告訴瀏覽器這個事件處理器會不會呼叫 event.preventDefault
來停止瀏覽器的原生行為。
<div @scroll.passive="onScroll">...</div>
這個屬性較常見在用來改善 scroll
事件的效能,因為以前瀏覽器要多判斷 scroll
事件會不會被 preventDefault
,
加上 passive
屬性之後就直接略過這個判斷,當 passive
為 true
表示此事件不會被 preventDefault
。
換句話說, .passive
修飾子無法與 .prevent
一起使用,因為這時 .prevent
會直接被無視。
# 鍵盤修飾子
在處理鍵盤事件時,我們常常會需要判斷使用者的按鍵來進行不同的動作,針對這類需求,
Vue.js 也很貼心地提供了鍵盤修飾子來對應 event.key
:
.enter
.tab
.delete
(包含del
與backspace
鍵).esc
.space
.up
.down
.left
.right
.ctrl
.alt
.shift
.meta
小提醒
.meta
在 window 對應的是視窗鍵 (⊞),在 mac 系統對應的是 command 鍵 (⌘)。
幾乎所有 event.key
在 Vue.js 都有對應的鍵盤修飾子,
不過需要注意的是像 PageDown
這類名稱要轉換成連字號 (kebab-case) .page-down
來使用。
換句話說,若你想在使用者按下 enter
鍵才觸發某個 method
時,就可以這樣做:
<!-- 當使用者在輸入框按下 enter 鍵後執行 addToMessages -->
<input type="text"
placeholder="輸入任意文字後按下 enter 鍵"
v-model.trim="msg"
@keydown.enter="addToMessages">
2
3
4
5
注意
由於 keyCode
的功能已經從網頁標準中移除 (opens new window),
Vue.js 自 3.0 之後也不再支援 keyCode
的用法 (參考 (opens new window)),請慎重使用。
# .exact
精確判斷
Vue.js 自 2.5 版後新增了 .exact
修飾子,用來精確判斷鍵盤的修飾子。
以上一個範例來說,當我們在 input
輸入框同時按下 Ctrl
+ Enter
鍵的時候也會執行 addToMessages
:
<input type="text"
placeholder="輸入任意文字後按下 enter 鍵"
v-model.trim="msg"
@keydown.enter="addToMessages">
2
3
4
但是像這樣,加上了 .exact
之後就只有在按下 Enter
鍵的時候才會執行 addToMessages
:
<input type="text"
placeholder="輸入任意文字後按下 enter 鍵"
v-model.trim="msg"
@keydown.enter.exact="addToMessages">
2
3
4
.exact
通常被用來作為系統按鍵的各種組合熱鍵判斷使用。
# 滑鼠修飾子
最後一個則是滑鼠事件專用的修飾子,用來提供滑鼠按鍵的點擊判斷。
.left
.right
.middle
# 拆解 v-model
的黑魔法
前一篇我們曾說過, v-model
並非什麼黑魔法,它本質上就是一種語法糖。
既然是語法糖,我們就可以用原有的方式來還原它。
以一個最常見的例子來說,
<input v-model="msg">
當我們拆解背後原理後,其實不難發現這個 v-model
實際上就等同於:
<input :value="msg" @input="msg = $event.target.value">
簡單來說,將 input
輸入框綁定了 value
屬性後,再針對它的 input
事件進行監聽,
當事件觸發時,便將 input
輸入框的 value
即時更新回 data
的 msg
屬性。
發現了嗎? 在一開始的 Vue.js MVVM 示意圖裡頭的 ViewModel 做的事情,就是 v-model
在做的事。
而不同的表單元素在 v-model
的實作上也有些許不同,如 input
的 text
和 textarea
元素對應的是 input
事件,
而 checkbox
、 radio
以及 select
在背後監聽的是 change
事件。 若再加上不同修飾子之後還會有不同的效用。
所以說,如果不用 v-model
,你也可以透過 v-on
與 v-bind
改變監聽的事件來自訂想要的行為。
而除了 HTML 原生元素以外, Vue.js 也能讓開發者針對自定義元件添加各種指令,甚至是自訂事件。 這部分我們留待到第二章元件篇時還會詳細為各位說明。