# 1-3 資料加工與邏輯整合
在上一個篇章當中,我們提到了 Vue.js 的模板語法,以及如何將實體的狀態 (data) 結合運算式結果渲染至畫面的方式。 但如果在好幾組模板語法置入相同的運算邏輯,或是在多處置入同樣的複雜運算式,這個模板就會變得難以維護。
以上一個小節的範例來說,要是今天需要在網頁的多個地方都要顯示「總金額」,假設三個好了:
<div id="app">
<p> 總金額共 {{ price * quantity }} 元 </p>
<!-- ... 中間略 ... -->
<p> 總金額共 {{ price * quantity }} 元 </p>
<!-- ... 中間略 ... -->
<p> 總金額共 {{ price * quantity }} 元 </p>
</div>
2
3
4
5
6
7
這個時候,由於重複的邏輯程式碼四散在網頁模板各處,在維護時很可能會出現遺漏修改的狀況。 同時,要是在模板置入了太過複雜的運算邏輯,不僅程式碼不夠直觀難以維護,嚴重的話還可能會產生不可預期的錯誤。
本篇,就來聊聊 Vue.js 是如何解決這些問題。
# methods 方法
不知道各位讀者有沒有聽過,在程式設計領域有個理論叫「三次法則」(rule of three)?
根據維基百科對「三次法則」理論的定義:
三次法則(rule of three)的要求是,允許按需直接複製貼上代碼一次,但如果相同的代碼片段重複出現三次以上的時候,將其提取出來做成一個子程式就勢在必行。 ...... 具體來說,當有代碼片段需要變更時,代碼維護者就必須找出程式中所有與之相同的代碼片段,並都進行修改,但這一過程易出差錯,而且也常會帶來許多麻煩。相對的,如果代碼只在一個地方出現,修改起來就容易多了。
鄉民都知道重要的事要講三次,但是資深碼農告訴你,同樣的程式要是寫三次,如果這是 Bug 的話你也得修正三次。
但通常你只會記得要修正被 PM / 客戶發現的那次就當完事了 ,於是相同的客訴也會來三次。
簡單來說,相同程式片段重複出現三次或以上的時候,在重構時我們就會將重複的部分提取出來並包裝成函式以便重複使用。
那麼,在 Vue.js 的實體裡面,我們也可以將這些重複使用的邏輯包裝起來,放在 methods
的屬性中。
延續前面的範例,我們在實體裡加上 methods
(要記得加 s) 屬性,並加入 subtotal
方法:
const vm = Vue.createApp({
data () {
return {
price: 100,
quantity: 10
}
},
methods: {
subtotal: function() {
return this.price * this.quantity;
}
}
}).mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
那麼在模板裡,我們就可以透過呼叫 subtotal()
的方式來取得總金額:
<div id="app">
<p> 總金額共 {{ subtotal() }} 元 </p>
<!-- ... 中間略 ... -->
<p> 總金額共 {{ subtotal() }} 元 </p>
<!-- ... 中間略 ... -->
<p> 總金額共 {{ subtotal() }} 元 </p>
</div>
2
3
4
5
6
7
這段範例有幾個重點需要注意。
首先,在 Vue.js 的實體裡面,我們可以透過 this
關鍵字來存取這個實體。
換句話說,在 data
裡面的屬性 price
以及 quantity
,到了 methods
裡面,
我們就可以透過 this.price
以及 this.quantity
來存取它們。
另外,由於 methods
裡面的內容實際上就是個 function
,在使用時需要被調用呼叫,所以在模板當中需要加入小括號,
如 subtotal()
,當然它也可以傳入參數。
當然要在實體中另一個 methods
呼叫 subtotal
就透過 this.subtotal()
即可。
但要小心,在模板內就不需要加上 this
。
有關 methods
的應用,在後續講解「事件」的部分我們還會再見到它。
小提醒: Vue 的 methods 能不能用箭頭函式 (arrow function) ?
自 JavaScript 的 ES6 起,新增了箭頭函式的語法 () => { }
,
雖然可以少打幾個字感覺比較爽,但需要注意的是它還有另一個作用: this
變數的強制綁定。
由於箭頭函式沒有自己的 this
,也就是說,在箭頭函式裡面使用 this
,實際上這個 this
跟外面的 this
是同一個。
Vue.createApp({
methods: {
subtotal: () => {
// 小心這裡的 this 由於 arrow function 的關係變成了 window!
return this.price * this.quantity;
}
}
});
2
3
4
5
6
7
8
像這樣,如果在 methods
裡面需要存取實體狀態的情況,就不適合使用箭頭函式。
但好在 ES6 針對物件的方法,可以簡寫成 subtotal () { ... }
的形式:
Vue.createApp({
methods: {
subtotal () {
// 沒問題! 這裡的 this 可以對應至 Vue 實體
return this.price * this.quantity;
}
}
});
2
3
4
5
6
7
8
(關於 JavaScript 一般函式的各種 this
綁定規則可見【0 陷阱!0 誤解!8 天重新認識 JavaScript!】中 "What's "THIS" in JavaScript" 小節,此處不再贅述。)
# computed 計算屬性
除了透過 methods
屬性來進行包裝, Vue.js 也提供了另一個屬性 computed
來處理類似的問題,讓我們將前面的程式碼用 computed
改寫:
const vm = Vue.createApp({
data() {
return {
price: 100,
quantity: 10
}
},
computed: {
subtotal: function() {
return this.price * this.quantity;
}
}
}).mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
<div id="app">
<p> 總金額共 {{ subtotal }} 元 </p>
<!-- ... 中間略 ... -->
<p> 總金額共 {{ subtotal }} 元 </p>
<!-- ... 中間略 ... -->
<p> 總金額共 {{ subtotal }} 元 </p>
</div>
2
3
4
5
6
7
以這個例子來說,computed
寫法看起來跟 methods
的寫法有 87% 像,除了將 methods
改成 computed
之外,差別就只有模板中的 subtotal
少了小括號。
這是否代表說,能放在 methods
的寫法就一定能用 computed
改寫呢? 讓我們來看看下面這個例子。
const vm = Vue.createApp({
data () {
return {
quantity: 100,
price: 100
}
},
computed: {
subtotalComputed: function() {
console.log('computed');
return this.quantity * this.price;
}
},
methods: {
subtotalMethods: function() {
console.log('methods');
return this.quantity * this.price;
}
}
}).mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
首先我們針對兩組相同程式碼的函式,分別放在 computed
與 methods
屬性中,然後模板是這樣的:
<div id="app">
<p> 總金額共 {{ subtotalComputed }} 元 </p>
<p> 總金額共 {{ subtotalMethods() }} 元 </p>
</div>
2
3
4
就執行的結果上,兩組的輸出都會是 100 * 100
的結果,也就是 10000
。
但這時我們將模板稍作改寫:
<div id="app">
<p> 總金額共 {{ subtotalComputed }} 元 </p>
<p> 總金額共 {{ subtotalComputed }} 元 </p>
<p> 總金額共 {{ subtotalComputed }} 元 </p>
<p> 總金額共 {{ subtotalMethods() }} 元 </p>
<p> 總金額共 {{ subtotalMethods() }} 元 </p>
<p> 總金額共 {{ subtotalMethods() }} 元 </p>
</div>
2
3
4
5
6
7
8
9
將 subtotalComputed
與 subtotalMethods()
都分別執行了三次,
由於我們在上面的兩個函式裡都寫了 console.log()
,所以我們打開 console 主控台會看到:
computed
methods
methods
methods
2
3
4
這樣的訊息。
發現了嗎? 同樣的程式碼, subtotalComputed
只執行了一次,而 subtotalMethods()
卻執行了三次。
這是由於 computed
屬性會將計算後的結果暫存,若它所觀察的屬性 (也就是那些 this.XXX
) 沒有被更新的情況下,computed
就不會重複被執行。
讓我們再把這個例子修改一下:
const vm = Vue.createApp({
data () {
return {
message: 'Hello Vue!'
}
},
computed: {
subtotalComputed: function() {
console.log('computed');
return 100 * 100;
}
},
methods: {
subtotalMethods: function() {
console.log('methods');
return 100 * 100;
}
}
}).mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="app">
<p> 總金額共 {{ subtotalComputed }} 元 </p>
<p> 總金額共 {{ subtotalMethods() }} 元 </p>
</div>
2
3
4
這裡我們拿掉了原本 data
裡的 quantity
與 price
,
加入了一個不重要的 message
,並且將 subtotalComputed
與 subtotalMethods
對 Vue 實體所依賴的 this
屬性,並改成寫死的數字。
雖說執行的結果跟原本沒有不同,但有趣的是,
當我們此時嘗試要去更新 data
裡的 message
屬性時,會觸發 methods
的呼叫,而 computed
卻不會再次執行。
這是因為 computed
會將計算後的結果暫存起來,並且只會在它所觀察的屬性,如 this.XXX
被更新的時候,才會再次執行對應的 function
。
小提醒
如果是同樣的重複計算,用 computed
來處理會比 methods
效能來得好,
但要注意的是, computed
屬性中的 function
無法使用參數,若有需要帶入參數的情況,還是只能使用 mehtods
來處理囉。
# computed / methods 的使用時機比較
那麼,什麼時候適合用 computed
,什麼時候適合用 methods
呢?
再給各位一個例子參考。
近年出國的人很多,尤其是日本,所以現在我們想要做個幣值轉換器,來計算台幣與日幣的轉換。
雖說疫情的關係,現在不能出國了,但我們還是可以在 Amazon.jp 使用日幣。
假設 1 日幣 = 0.278 台幣,那麼我們也許可以這樣開始寫:
const vm = Vue.createApp({
data () {
return {
twd: 0.278,
jpy: 1,
}
}
}).mount('#app');
2
3
4
5
6
7
8
<div id="app">
<p>1 日幣 = 0.278 台幣</p>
<div>台幣 <input type="text" v-model="twd"></div>
<div>日幣 <input type="text" v-model="jpy"></div>
</div>
2
3
4
5
小提醒
這裡的 v-model
語法,是用來讓 input
的輸入框與我們 Vue 實體內的屬性做綁定的作用,詳細解說後面會提到。
各位讀者只要知道這裡放上 v-model
,使用者輸入的內容會跑進 Vue 實體對應的屬性即可。
當然現在還沒有任何功能,我們只是把兩個欄位分別與 data
的 twd
與 jpy
屬性來做綁定。
接下來要開始做連動功能,所以我們加上 twd2jpy
與 jpy2twd
兩個 method 來計算。
<div id="app">
<p>1 日幣 = 0.278 台幣</p>
<div>台幣 <input type="text" v-model="twd" v-on:input="twd2jpy"></div>
<div>日幣 <input type="text" v-model="jpy" v-on:input="jpy2twd"></div>
</div>
2
3
4
5
methods: {
twd2jpy () {
this.jpy = Number.parseFloat(Number(this.twd) / 0.278).toFixed(3);
},
jpy2twd () {
this.twd = Number.parseFloat(Number(this.jpy) * 0.278).toFixed(3);
},
}
2
3
4
5
6
7
8
執行結果會像這樣
小提醒
依然是劇透,這裡出現的 v-on:input
指的是監聽輸入框的 input
事件,
當輸入框元素指定的事件被觸發時,就會去執行對應的方法,後面還會詳細介紹。
看起來很不錯呢! 就決定用 mehtods
了!
從遠方看到 PM 又走來了 (哪來的 PM) ,並問說「哇這麼厲害,那可不可以也幫我加上美金、人民幣的轉換啊」?
程序猿心想,如果要加上人民幣與美金的話,那是不是還得再加入 twd2usd
、 twd2rmb
、 jpy2usd
、 jpy2rmb
、 usd2twd
、 usd2jpy
、 usd2rmb
等各種轉換幣值的方法?
(程序猿吐血而亡)
看到這裡,相信你已經發現問題出在哪了吧!
在做幣值的計算,其實不管台幣換日幣,或是日幣換台幣,從一開始我們就只需要一種基準值,不管怎麼換,錢都是一樣的。
換言之,程式可以改寫成這樣:
<div id="app">
<p>1 日幣 = 0.278 台幣</p>
<div>台幣 <input type="text" v-model="twd"></div>
<div>日幣 <input type="text" v-model="jpy"></div>
</div>
2
3
4
5
我們將 jpy
從原本的 data
屬性,改由 computed
來生成,
並且為 jpy
加入 get
與 set
的 function:
const vm = Vue.createApp({
data () {
return {
twd: 0.278
}
},
computed: {
jpy: {
get () {
return Number.parseFloat(Number(this.twd) / 0.278).toFixed(3);
},
set (val) {
this.twd = Number.parseFloat(Number(val) * 0.278).toFixed(3);
}
}
}
}).mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
這裡補充一個剛才沒有提到的觀念, computed
除了可以單純將另一個實體屬性「加工」後回傳 (也就是預設的 get
) 之外,
還可以透過 set
寫回這個 computed
屬性,就像上面的 jpy
一樣。
注意
若沒有為 computed
屬性加上 set
,則不允許手動修改對應的 computed
屬性。
否則在 console 主控台會出現類似: [Vue warn]: Computed property "jpy" was assigned to but it has no setter.
的錯誤訊息。
此時我們的 data
就只留下 twd
作為基準, 而 jpy
則是透過 computed
來對 twd
做加工計算,
並在 jpy
更新的時候,透過 set
回去改寫 twd
的數值。
往後,就算我們要加上美金也只需要在 computed
屬性加上 usd
:
usd: {
get () {
return Number.parseFloat(Number(this.twd) / 28.540).toFixed(3);
},
set (val) {
this.twd = Number.parseFloat(Number(val) * 28.540).toFixed(3);
}
}
2
3
4
5
6
7
8
相信未來若要再加入其他幣值的換算,肯定也不是難事了!
# 補充 - Vue Filters (Vue 2.x)
Vue.js 還有一個與 methods
很像的屬性,名為 filters
,之所以沒有特別列一篇出來介紹,
是因為在實際開發上它使用的機會真的不多,而且在 Vue.js 3.0 這個特性會被移除。
這裡就以補充的形式,快速為讀者們說明這個在 Vue 3.0 已經被移除的屬性。
Vue filters
的用法與其他 options API 一樣,以物件的形式在實體內定義一個 filters
屬性,如:
// for Vue 2.x
const vm = new Vue({
data: {
msg: 'hello!'
},
filters: {
capitalize: (value) => {
if (!value) {
return '';
}
value = value.toString();
return value.charAt(0).toUpperCase() + value.slice(1);
},
}
}).$mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
這個 capitalize
的 filters
會將傳入字串的首字轉為大寫字母後回傳。
在 HTML 模板中,我們用一個 |
(pipe) 符號來表示它:
<p>{{ msg }}</p>
<p>{{ msg | capitalize }}</p>
2
像這樣,瀏覽器顯示的結果會是:
hello!
Hello!
2
filters
會將 |
(pipe) 符號前面的值當作參數引入,並且回傳對應的內容。看起來跟 methods
的作用很像對吧?
如果將 capitalize
寫成 methods
就會變成這樣:
// for Vue 2.x
const vm = new Vue({
data: {
msg: 'hello!'
},
methods: {
capitalize (value) {
if (!value) {
return '';
}
value = value.toString();
return value.charAt(0).toUpperCase() + value.slice(1);
}
}
}).$mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<p>{{ msg }}</p>
<p>{{ capitalize(msg) }}</p>
2
兩者的結果是一樣的。
不過需要注意的是,filters
的 function
裡面是 "無法" 透過 this
來取得 Vue 實體的,
當然也沒辦法對 data
內的狀態做任何額外處理。
你可以將 filters
視為 「Pure Function」,將相同的輸入丟入,永遠都會回傳相同的輸出,並且不對任何該函數以外的任何作用域產生影響。
所以一直以來,filters
都只被用來處理文字或屬性的格式化。
但到了 Vue 3.0 之後,由於 Composition API 的出現, filters
不管在使用時機、實用度都與 methods
有著高度的重疊,執行的效能也沒有差太多,於是在 Vue 3.0 就成為少數被廢棄的特性了。