2019年2月12日 星期二

Vue 學習筆記 (三) : computed (計算函數) 與 filters (過濾器) 測試

在前一篇測試中, 要從 Vue 物件透過 Mustache 樣版引擎將物件中 data 屬性所儲存的變數即時傳遞到網頁中, 只要使用 Mustache 標籤 {{ propertyName }} 將變數綁定至標籤內即可. 事實上, Mustache 標籤 {{ }} 內還可以放置運算式或加上所謂的 filters (過濾器) 在變數渲染前做一些前置處理.

本系列之前的測試文章參考 :

關於前端網頁框架 Vue
Vue 學習筆記 (一) : 環境配置
Vue 學習筆記 (二) : MVVM 架構與雙向資料綁定

本測試參考了下列書籍中的範例加以改寫 :
  1. 一次搞懂熱門前端框架 (旗標)
  2. The Majesty of Vue (Packt)
  3. Vue.js 2 Cookbook (Packt)
  4. Vue.js 2.x by Example (Packt)
關於 Mustache 樣版 (Template) 語法參考 :

https://vuejs.org/v2/guide/syntax.html


一. Mustache 樣版運算式 :

在 Mustache 標籤內可以使用 Javascript 運算式 (Expression) 對 Vue 物件傳遞過來的變數進行運算, 運算可在 Mustache 模版標籤內或在 App 的 data 物件內進行, 但只限於簡單的單行運算式, 且只能使用有限的全域物件例如 Date 或 Math 物件, 不可使用自訂義之全域變數, 也不可使用 Javascript 敘述 (Statements), 邏輯判斷或迴圈等指令, 參考 :

https://vuejs.org/v2/guide/syntax.html#Using-JavaScript-Expressions

例如 :

測試 1 : Mustache 運算式  [原始碼]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="https://unpkg.com/vue"></script>
  </head>
  <body>
    <div id='msg'>
      <p>{{ 2+3 }}</p>
      <p>{{ 'Hello' + 'World!' }}</p>
      <p> There are {{ 5 }} people here.</p>
      <p> We need {{ deskCount }} more desks.</p>
      <p> 陣列 : {{ characters }}</p>
      <p> 物件 : {{ car }}</p>
      <p> 主演 : {{ characters.join('&') }}</p>
      <p> 男主角 : {{ characters[1] }}</p>
      <p> 廠牌 : {{ car.type }}</p>
    </div>
    <script>
      const app=new Vue({
        el: '#msg',
        data: {
          deskCount: 10 + 20,
          characters: ['李世榮','呂珍九'],
          car: {type:"Fiat", model:"500", color:"white"}
          }
        });
    </script>
  </body>
</html>

此 App 綁定了 deskCount, characters, 以及 car (物件) 三個資料變數 (可變), 其餘都是在 Mustache 直接進行 Javascript 的固定運算 (不可變). 在 Mustache 樣版中使用了陣列方法 join(), 陣列索引 [1], 以及物件屬性 .type 等運算與計算式來控制最後網頁輸出的結果.  執行結果如下 :




綁定的資料變數與網頁呈現為即時雙向互動的, 亦即不論是使用者或 App 更改了資料, 另一方也會立即改變. 例如只要在 App 中透過修改 app.car.type 的屬性值, 則網頁視圖中的廠牌也會立刻被修改.

在 Chrome 瀏覽器中按 Ctrl + Shift + I 進入開發人員模式, 然後切到 Console (控制台) 頁籤, 在 > 提示號後面輸入 app.car.type='Toyota' 後按 Enter 即可看到網頁中的廠牌會從原本的 Fiat 立刻被改為 Toyota :




上面的範例是在 Mustache 樣版標籤內進行簡單的 Javascript 運算, 但只限於可直接輸出的運算式. 事實上 Vue 提供了 computed 計算屬性, 可在 App 內定義一個可於 Mustache 標籤內呼叫的無參數函數, 用法如下 :

computed: {
    doSomething() {
        //result=calculation or assignment
        return result;
        }
    }   

或者寫成下面的形式也可以 :

computed: {
    doSomething: function() {
        //result=calculation or assignment
        return result;
        }
    }

此 computed 屬性中定義了一個無參數函數 doSomething(), 經過運算後以 return 傳回結果, 這時在 Mustache 標籤內使用函數名稱便能呼叫此函數 (不可加括弧), 輸出值即為函數之傳回值 :

<p> {{ doSomething }} </p>

例如下面範例會輸出 1+2+3+...+10 之和, 根據等差級數和公式 n(A1+An)/2=10*(1+10)/2=55 :

測試 2 : 用 computed 屬性定義計算函數 (1)  [原始碼]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="https://vuejs.org/js/vue.js"></script>
  </head>
  <body>
    <div id='msg'>
      <p> {{ sum }}</p>
    </div>
    <script>
      new Vue({
        el: '#msg',
        computed: {
          sum() {
            var total=0
            for (var i=1; i<=10; i++) {
              total += i;
              }
            return total;
            }
          }
        });
    </script>
  </body>
</html>

由於 computed 定義的計算函數無法傳遞參數, 因此上面的程式只能執行固定的計算 (等差為 1 的級數末項=10). 如果要用參數動態更改計算結果可以利用 data 屬性綁定網頁上的輸入欄位來達成, 例如 :

測試 3 : 用 computed 屬性定義計算函數 (2)  [原始碼]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="https://vuejs.org/js/vue.js"></script>
  </head>
  <body>
    <div id='msg'>
      <p> {{ '1+2+3+...+' + number + '=' + sum }}</p>
      <input type='text' v-model="number">
    </div>
    <script>
      new Vue({
        el: '#msg',
        data: {
          number: '10'
          },
        computed: {
          sum() {           //寫成 sum: function() { 也可以
            var total=0
            for (var i=1; i<=this.number; i++) {
              total += i;
              }
            return total;
            }
          }
        });
    </script>
  </body>
</html>




此例網頁中新增了一個 input 文字欄位來設定級數的末項, 然後將此欄位與 Vue 物件的 data 屬性之 number 變數綁定, 如此一來使用者在網頁上輸入的數字立即會更新 number 變數. 而在 computed 屬性定義之計算函數 sum() 中則用 this.number 取得 Vue 物件裡的 number 變數並設為迴圈之終止條件, 只要輸入的數值被改變, 則 sum() 函數計算之總和也會立即更新並輸出到 p 元素中, 因此 computed 中的回傳變數 sum 也是與 p 元素中的 sum 綁定的.

不過上面的範例中, computed 屬性定義的計算函數只能從 Vue 物件的 data 屬性中取得資料 (data 的 getter), 計算的結果只能回傳給 Mustache 樣版引擎輸出, 可否不回傳直接去設定 data 屬性中的變數呢 (data 的 setter)? 可以的, 但是 computed 屬性須以下列格式設定 :

computed: {
    property:  {
        get: function() {
            //result=calculations by getting variables from data (this.variable)
            return result;
            },
        set: function(val) {
             //setting variables in data with val, e.g. : this.variable=calculations of val
             }
        }
    }

此處 property 屬性定義了兩個計算函數 get 與 set, 其中 get 功能與上面兩個範例一樣, 就是從 data 屬性中取得變數值, 運算後傳回給屬性 property; 而 set 函數用來設定 data 屬性中變數 (不傳回值). set 函數的傳入參數可以用任何識別字, val 只是任選的, 事實上它就是與網頁輸入元素綁定的 property.

例如下面這個華氏溫度與攝氏溫度換算的範例 :

測試 4 : 用 computed 屬性定義計算函數 (3)  [原始碼]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="https://vuejs.org/js/vue.js"></script>
  </head>
  <body>
    <div id='msg'>
      攝氏(C)<input type='text' v-model="celsius"> = 
      華氏(F)<input type='text' v-model="fahrenheit">
    </div>
    <script>
      new Vue({
        el: '#msg',
        data: {
          celsius: 0
          },
        computed: {
          fahrenheit: {
            get: function() {
              return Math.round(100*(this.celsius*(9/5)+32))/100;  //傳回 fahrenheit
              },
            set: function(val) {
              this.celsius=Math.round(100*((val-32)*5/9))/100;  //設定 data 內變數
              }
            }
          }
        });
    </script>
  </body>
</html>




此例網頁中有兩個文字輸入欄位, 分別綁定 Vue 物件中 data 屬性裡的 celsius 變數與 computed 屬性裡的 fahrenheit 變數. 不論在哪一個欄位輸入數值, 另一個欄位馬上就會更新.

若輸入攝氏度數, 則與它綁定的 data 屬性之 celsius 變數立刻被改變, 那麼 computed 計算函數中的 get 函數會被呼叫, 利用 this.celsius 取得所綁定之輸入值代入公式計算出華氏度數後傳回給 fahrenheit, 而此變數與華氏 input 欄位綁定, 因此會立即更新輸出值.

若輸入華氏度數, 則與它綁定的 computed 屬性之 fahrenheit 變數立刻被改變, 函數 set 會被呼叫並傳入華氏度數給 val 參數, 代入公式計算出攝氏度數後利用 this.celsius 設定 data 內之變數 celsius, 由於此變數與攝氏輸入欄位綁定, 因此輸出會立即更新.

注意, 由於 Javascript 的 Math.rround() 函數無法指定四捨五入到小數點後指定位數, 此處先將原值乘以 100 後取四捨五入再除以 100, 這樣便能取四捨五入到小數點後兩位了 (若至小數點後三位, 就先乘 1000 再除 1000). 參考 :

# JavaScript 四捨五入、無條件捨去、無條件進位


二. 過濾器 :

除了可用 computed 屬性在 Vue 物件內自行定義 Javascript 函數於 Mustache 樣版標籤內呼叫外, Vue 也內建了一些稱為過濾器 (filter) 的常用函數可直接於樣版標籤內串接 (呼叫) 使用. 簡言之, 過濾器其實是一組特定用途的函數, 用來把欲輸出之變數經過處理為特定格式後再透過 Mustache 樣版引擎輸出.

在 "一次搞懂熱門前端框架" 這本書的 4-2 節列出如下的過濾器 :

 Vue 的過濾器 說明
 json 將物件轉換成 JSON 格式
 capitalize 將第一個英文單字首字母大寫, 其餘小寫
 uppercase 將所有字母轉為大寫
 lowercase 將所有字母轉為小寫
 pluralize 將英文單字轉為複數
 currency 將數值每三位數家逗號, 並加上指定之貨幣單位
 debounce 延遲指定時間 (單位 ms) 後顯示, 例如 debounce 50 為延遲 50 ms 後輸出
 orderBy 將物件陣列以指定之屬性值排列, 例如 orderBy age 依據屬性 age 排序輸出 
 limitBy 限制陣列輸出範圍, 例如 limitBy 3, 2 表示從索引 2 開始輸出 3 個元素
 filterBy 過濾陣列輸出範圍, 例如 filterBy 'th' 只輸出 'th' 開頭的元素

過濾器可以依序用管線符號 "|" 串接, 格式如下 :

{{ propertyName | filter1 | filter2 | filter3 | ... }}

不過從 Vue 2 版後這些過濾器已經從 Vue 核心移除, 在 Vue 1 的最後版本 1.0.8 版還可使用, 例如 :

測試 5 : 過濾器測試 : (1)  [原始碼]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.8/vue.js"></script>
  </head>
  <body>
    <div id='msg'>
      <p>{{ message | lowercase }}</p>
      <p>{{ message | uppercase }}</p>
      <p>{{ 'hello world!' | capitalize }}</p>
      <p>{{ message | pluralize }}</p>
      <p>{{ 'Debounce 1 seccond' | debounce 1000 }}</p>
      <p>{{ 1000000 | currency }}</p>
    </div>
    <script>
      new Vue({
        el: '#msg',
        data: {
          message: 'Hello World!'
          }
        });
    </script>
  </body>
</html>

執行結果如下 :

hello world!                  (lowercase)
HELLO WORLD!        (uppercase)
Hello world!                 (capitalize)
undefineds                    (pluralize)
function () { context = this args = arguments timestamp = Date.now() if (!timeout) { timeout = setTimeout(later, wait) } return result }     (debounce)
$1,000,000.00               (currency)

其中 pluralize 未定義, 而 debounce 竟然顯示實作之程式碼, 真是太奇怪了. 如果用 Vue 2 執行, 這些過濾器均無作用, 例如 :

測試 5-1 : 過濾器測試 : (2)  [原始碼]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.4/vue.js"></script>
  </head>
  <body>
    <div id='msg'>
      <p>{{ message | lowercase }}</p>
      <p>{{ message | uppercase }}</p>
      <p>{{ 'hello world!' | capitalize }}</p>
      <p>{{ message | pluralize }}</p>
      <p>{{ 'Debounce 1 seccond' | debounce 1000 }}</p>
    </div>
    <script>
      new Vue({
        el: '#msg',
        data: {
          message: 'Hello World!'
          }
        });
    </script>
  </body>
</html>

用 Vue 2.6.4 版執行結果如下 (全無作用) :

Hello World!
Hello World!
hello world!
Hello World!
Debounce 1 seccond

用 Chrome 的開發人員工具檢查 Console 會發現有錯誤 :




因此下面還是用 Vue 1.0.8 版來測試其餘過濾器, 例如 :

測試 6 : 過濾器測試 : (3)  [原始碼]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.8/vue.js"></script>
  </head>
  <body>
    <div id='msg'>
      <p>{{ arr }}</p>
      <p>{{ arr | json }}</p>
      <p>{{ obj }}</p>
      <p>{{ obj | json }}</p>
      <p>{{ arr | filterBy 'A' }}</p>
      <p>{{ arr | limitBy 1,2 }}</p>
      <p>{{ obj | orderBy name | json }}</p>
    </div>
    <script>
      new Vue({
        el: '#msg',
        data: {
          arr: ['Tony', 'Amy', 'Peter', 'Kelly'],
          obj: [{name:'Kelly', age:20},{name:'Amy', age:16}]
          }
        });
    </script>
  </body>
</html>

此範例中定義了兩個變數 : 陣列 arr 與物件 obj. 執行結果如下 :

Tony,Amy,Peter,Kelly
[ "Tony", "Amy", "Peter", "Kelly" ]
[object Object],[object Object]
[ { "name": "Kelly", "age": 20 }, { "name": "Amy", "age": 16 } ]
Amy
Tony,Amy,Peter,Kelly
[ { "name": "Kelly", "age": 20 }, { "name": "Amy", "age": 16 } ]

可見直接輸出陣列會顯示所有元素內容, 直接輸出物件則只顯示 object Object 而已, 若使用過濾器 json, 則會轉成 JSON 格式輸出. filterBy 有作用, 但 limitBy 與 orderBy 卻沒有作用.

過濾器也可以自訂, Vue 提供了 filters 屬性來自訂過濾器, 如同 computed 屬性, 事實上也是定義一個計算函數, 格式如下 :

filters: {
    filterName: function(val) {
        //result=calculation in terms of val
        return result;
        }
    }

其中傳入參數 val 就是傳給過濾器的資料, 經過自訂函數處理後用 return 回傳給 Mustache 樣版引擎輸出, 例如 :

測試 7 : 自訂過濾器  [原始碼]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="https://vuejs.org/js/vue.js"></script>
  </head>
  <body>
    <div id='msg'>
      <p>{{ message | capitalize }}</p>
    </div>
    <script>
      new Vue({
        el: '#msg',
        data: {
          message: 'hello world!'
          },
        filters: {
          capitalize: function (val) {
            if (!val) {return ''}
            else {
              value=val.toString()
              return val.charAt(0).toUpperCase() + val.slice(1)
              }
            }
          }
        });
    </script>
  </body>
</html>

過濾器 filters 與計算函數 computed 功能差不多, 但過濾器適合用在較簡單的運算例如格式轉換; 而計算函數 computed 則適合用在複雜的運算上.

參考 :

vue2-filters
https://vuejs.org/v2/guide/filters.html
Filters in Depth
用範例理解 Vue.js #7:Filters vs Computed
Vue.js: Filter 過濾器


2019-12-17 補充 :

離開 Vue 一段時間了, 今天看到下面這篇不錯的文章, 記一下有空再研究 :

Vue中容易被忽視的知識點 (程式前沿)

沒有留言 :