# 1-3 資料加工與邏輯整合

在上一個篇章當中,我們提到了 Vue.js 的模板語法,以及如何將實體的狀態 (data) 結合運算式結果渲染至畫面的方式。 但如果在好幾組模板語法置入相同的運算邏輯,或是在多處置入同樣的複雜運算式,這個模板就會變得難以維護。

以上一個小節的範例來說,要是今天需要在網頁的多個地方都要顯示「總金額」,假設三個好了:

<div id="app">
  <p> 總金額共 {{ price * quantity }} 元 </p>
  <!-- ... 中間略 ... -->
  <p> 總金額共 {{ price * quantity }} 元 </p>
  <!-- ... 中間略 ... -->
  <p> 總金額共 {{ price * quantity }} 元 </p>
</div>
1
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');
1
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>
1
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;
    }
  }
});
1
2
3
4
5
6
7
8

像這樣,如果在 methods 裡面需要存取實體狀態的情況,就不適合使用箭頭函式。 但好在 ES6 針對物件的方法,可以簡寫成 subtotal () { ... } 的形式:



 
 
 
 



Vue.createApp({
  methods: {
    subtotal () {
      // 沒問題! 這裡的 this 可以對應至 Vue 實體
      return this.price * this.quantity;
    }
  }
});
1
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');
1
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>
1
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');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

首先我們針對兩組相同程式碼的函式,分別放在 computedmethods 屬性中,然後模板是這樣的:

<div id="app">
  <p> 總金額共 {{ subtotalComputed }} 元 </p>
  <p> 總金額共 {{ subtotalMethods() }} 元 </p>
</div>
1
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>
1
2
3
4
5
6
7
8
9

subtotalComputedsubtotalMethods() 都分別執行了三次, 由於我們在上面的兩個函式裡都寫了 console.log(),所以我們打開 console 主控台會看到:

computed
methods
methods
methods
1
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');
1
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>
1
2
3
4

這裡我們拿掉了原本 data 裡的 quantityprice, 加入了一個不重要的 message,並且將 subtotalComputedsubtotalMethods 對 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');
1
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>
1
2
3
4
5

示意圖1

小提醒

這裡的 v-model 語法,是用來讓 input 的輸入框與我們 Vue 實體內的屬性做綁定的作用,詳細解說後面會提到。 各位讀者只要知道這裡放上 v-model ,使用者輸入的內容會跑進 Vue 實體對應的屬性即可。

當然現在還沒有任何功能,我們只是把兩個欄位分別與 datatwdjpy 屬性來做綁定。 接下來要開始做連動功能,所以我們加上 twd2jpyjpy2twd 兩個 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>
1
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);
  },
}
1
2
3
4
5
6
7
8

執行結果會像這樣

試一試

小提醒

依然是劇透,這裡出現的 v-on:input 指的是監聽輸入框的 input 事件, 當輸入框元素指定的事件被觸發時,就會去執行對應的方法,後面還會詳細介紹。

看起來很不錯呢! 就決定用 mehtods 了!

等等!

從遠方看到 PM 又走來了 (哪來的 PM) ,並問說「哇這麼厲害,那可不可以也幫我加上美金、人民幣的轉換啊」?

程序猿心想,如果要加上人民幣與美金的話,那是不是還得再加入 twd2usdtwd2rmbjpy2usdjpy2rmbusd2twdusd2jpyusd2rmb 等各種轉換幣值的方法?

(程序猿吐血而亡)

看到這裡,相信你已經發現問題出在哪了吧!

在做幣值的計算,其實不管台幣換日幣,或是日幣換台幣,從一開始我們就只需要一種基準值,不管怎麼換,錢都是一樣的

換言之,程式可以改寫成這樣:

<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>
1
2
3
4
5

我們將 jpy 從原本的 data 屬性,改由 computed 來生成, 並且為 jpy 加入 getset 的 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');
1
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);
  }
}
1
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');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

這個 capitalizefilters 會將傳入字串的首字轉為大寫字母後回傳。

在 HTML 模板中,我們用一個 | (pipe) 符號來表示它:

  <p>{{ msg }}</p>
  <p>{{ msg | capitalize }}</p>
1
2

像這樣,瀏覽器顯示的結果會是:

hello!
Hello!
1
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');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  <p>{{ msg }}</p>
  <p>{{ capitalize(msg) }}</p>
1
2

兩者的結果是一樣的。

不過需要注意的是,filtersfunction 裡面是 "無法" 透過 this 來取得 Vue 實體的, 當然也沒辦法對 data 內的狀態做任何額外處理。

你可以將 filters 視為 「Pure Function」,將相同的輸入丟入,永遠都會回傳相同的輸出,並且不對任何該函數以外的任何作用域產生影響。

所以一直以來,filters 都只被用來處理文字或屬性的格式化。

但到了 Vue 3.0 之後,由於 Composition API 的出現, filters 不管在使用時機、實用度都與 methods 有著高度的重疊,執行的效能也沒有差太多,於是在 Vue 3.0 就成為少數被廢棄的特性了。

Last Updated: 1/2/2021, 1:30:37 AM