---
# System prepended metadata

title: C_重新認識Vue.js筆記
tags: [Vue]

---

[TOC]
## 1-1 Vue.js 簡介
### 漸進式

![image](https://hackmd.io/_uploads/B1NsF03xyg.png)
紅色部分是最核心的特色，可以依照專案需求漸進式套用其他需要的東西

### 指令式渲染 vs. 宣告式渲染

原生JS以「操作 DOM 為基礎」的模式為指令式渲染
取得元素，操控元素，一個指令一個動作
很多元素的時候會很亂，無法管理
![image](https://hackmd.io/_uploads/rJhi5Chl1g.png)

Vue.js則是將資料/狀態統一由 JS 的**物件**來維護管理，事件改變物件內的狀態，狀態被修改後再去更新模板的內容，也就是MVVM模式
```html
<div id="demo">
  <h1>{{ message }}</h1>
  <input v-model="message">
</div>
```

```javascript
// 建立一個Vue的物件實體，並且將這個物件指定至變數vm之中。
const vm = Vue.createApp({
  data() {
    return {
      message: 'Hello Vue!'
    }
  }
}).mount('#demo'); // 宣告後馬上掛載到指定的網頁節點

// 也可以之後再掛載
vm.mount('#demo');
```
這邊的`{data(){...}}`是稱為`options`的物件參數
透過`mount`，表示`vm`這個 Vue.js 實體物件 (根元件) 可控制的範圍為`#demo`這個節點

### MVVM 模式
![image](https://hackmd.io/_uploads/BkLd6A3ekl.png)
將 DOM 的事件監聽與狀態的資料綁定封裝起來 (ViewModel, VM)
使用者操作 View (V)
透過 VM 內的 Vue.js 把狀態回存至 Model (M) (某個物件，以上面的程式碼來說就是 `vm` ?)
Model 被更新後 VM 內的 Vue.js 也會同步更新網頁模板 (V)

### DOMContentLoaded
如果程式碼寫在`<head>`裡面會因為瀏覽器還沒解析到`<body>`內的DOM而出錯，可以利用`DOMContentLoaded`事件等到DOM完全載入後再執行
```javascript
const vm = Vue.createApp({
  // 略
});

document.addEventListener("DOMContentLoaded", () => {
  // DOM Ready!
  vm.mount('#app');
});
```

## 1-2 Vue.js 的核心: 實體
### 把實體掛載至 DOM
方法一：在`options`設定`el`屬性 (3.0不能用)
```javascript
// for Vue 2.x
const vm = new Vue({
  el: '#app'
});
```
方法二：用`$mount` (2.x) 或 `mount` (3.0) 方法
```javascript
// 新增節點然後加入至 body
const el = document.createElement('div');
document.body.appendChild(el);

// Vue 2.x
vm.$mount(el);

// Vue 3.0
vm.mount('#app');
```
`el`屬性和`mount()`設定的節點可以是 CSS 選擇器，也可以是 DOM 物件

:heavy_check_mark:可以用`vm.mount(el);`嗎
可以！
#### 測試結果
:::spoiler
```javascript
import {
  createApp,
  ref,
} from "https://unpkg.com/vue@3/dist/vue.esm-browser.js";

const el = document.createElement("div");
el.setAttribute("id", "child");
el.textContent = "{{ name }}已陣亡";
document.body.appendChild(el);

const vm = createApp({
  data() {
    return {
      name: "天兔",
    };
  },
  mounted() {
    console.log("呀哈");
  },
});

vm.mount(el);
```
>![image](https://hackmd.io/_uploads/rkkyQAFM1e.png)
:::

#### 測試失敗紀錄
:::spoiler
在實作的時候一直失敗，看到官方文件這句話
>![image](https://hackmd.io/_uploads/rkKZakMz1l.png)

而檢查檔案中有使用到.mount()的地方只有main.js
![image](https://hackmd.io/_uploads/Sk_P6JzGye.png)
所以猜測，在App.vue裡面集合了所有要拿來製作實體的東西(?)，然後一次在App.vue中用createApp()創造這個實體，並且掛載上去
:::





### 定義狀態
在 Vue 實體的定義的狀態，就是透過 `data` 屬性來儲存。
`data` 屬性是function的形式
```javascript
const vm = Vue.createApp({
  // 實體所回傳的狀態會以物件 key-value 的形式
  // 且在 Vue 3.0 開始，data 將強制以 function 的形式出現
  data () {
    return {
      name: '008JS'
    }
  }
});
```

### 模板語法
採用的是 Mustache 語法
> [從 Mustache 的 render 函式了解模板系統如何解析並渲染傳入的資料](https://uu9924079.medium.com/從-mustache-的-render-函式了解模板系統如何解析並渲染傳入的資料-e32f4ca4dd50)

利用`{{ }}`把定義的狀態輸出到html上
```html
<div id="app">
  {{ name }} 好棒棒!
</div>
```
![image](https://hackmd.io/_uploads/SJkZzL1byg.png)

理解：也就是在data內存放變數，可以利用這個變數直接在html上渲染
Vue.js 會自動將 data 內的屬性加上 getter 與 setter 的特性，以便監控狀態的更新 (響應式更新)。
在 Vue.js 的實體當中，以底線 _ 或錢字號 $ 作為開頭的屬性，不會被加上 getter 與 setter 的特性，命名時避免使用_和$作開頭

定義的狀態要怎麼更新？透過 `vm.$data.XXX` 來操作內部狀態。(注意 vm.mount('#app') 之後才會產生對應的 $data)
```javascript
const vm = Vue.createApp({
  // 實體所回傳的狀態會以物件 key-value 的形式
  // 且在 Vue 3.0 開始，data 將強制以 function 的形式出現
  data () {
    return {
      name: '008JS'
    }
  }
}).mount('#app'); // 建立實體的同時要掛載到DOM上!!!

vm.$data.name = "30cm"; // 才能更改
```

如果建立實體的同時沒有掛載到DOM上，就無法取得$data
```javascript
const vm = createApp({
  data() {
    return {
      name: "天兔",
    };
  },
  mounted() {
    console.log("呀哈");
  },
});

vm.mount(el);

vm.$data.name = "小八";
```
>![image](https://hackmd.io/_uploads/BklQORYMke.png)
```javascript
const vm = createApp({
  data() {
    return {
      name: "天兔",
    };
  },
  mounted() {
    console.log("呀哈");
  },
}).mount(el);

vm.$data.name = "小八";
```
>![image](https://hackmd.io/_uploads/SyTBdCFzyx.png)

在`{{ }}`內也可以進行變數之間的運算，最後將算出來的值輸出到畫面上

`{{ }}`可以自定義更改，例如改成`%{ }%`，設定`delimiters`屬性。但這個屬性只支援在「瀏覽器」即時編譯的版本中使用。

```javascript
const vm = Vue.createApp({
  delimiters: [`%{`,`}%`],
  data () {
    return {
      name: '008JS'
    }
  }
});
```
### 共用 data 的汙染
定義在Vue實體外的data，可以讓不同的Vue實體共用這個data的設定，但更改實體A的data也會同時改變實體B的狀況，建議使用淺拷貝或深拷貝`JSON.parse(JSON.stringify(...))`的方式引入實體A和實體B

### template 模板屬性
除了直接用`{{ }}`寫在html，也可以使用`template`屬性
```javascript
const vm = Vue.createApp({
  template: `<div>{{ greeting }} 好棒棒!</div>`,
  data () {
    return {
      greeting: 'Hello Vue.js!'
    }
  }
}).mount('#app');
```

## 1-3 資料加工與邏輯整合
### methods 方法
重複的程式片段包成function，function可以放在`methods`屬性，並作為一個物件的屬性名稱。
```javascript
const vm = Vue.createApp({
  data () {
    return {
      price: 100,
      quantity: 10,
    }
  },
  methods: {
    subtotal: function () { // 小心不要寫成箭頭函式
      return this.price * this.quantity; // 記得要加this
    }
    
    // 但ES6的方法可以簡寫成這樣
    sum () {
      return this.price * this.quantity; 
    }
  }
}).mount('#app');
```
> [ES6的方法簡寫](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Method_definitions)

在模板中呼叫這個方法就可以使用了
```html
<div id="app">
  總額是{{ subtotal() }}元
</div>
```

### computed
和methods很像，使用上不用小括號執行
```javascript
const vm = Vue.createApp({
  data () {
    return {
      price: 100,
      quantity: 10,
    }
  },
  computed: {
    subtotal: function () { 
      return this.price * this.quantity; 
    }
  }
}).mount('#app');
```
```html
<div id="app">
  <!--  不用小括號  -->
  總額是{{ subtotal }}元
</div>
```
和methods的差別在於，computed只會計算一次並把結果暫存起來，直到它裡面的屬性(price, quantity之類的)被更新，才會再次執行

所以執行效率上computed比較快，但缺點是computed中的function無法帶入參數(因為不用小括號)

:heavy_check_mark:當我們此時嘗試要去更新 data 裡的 message 屬性時，會觸發 methods 的呼叫，而 computed 卻不會再次執行。

#### 測試結果
:::spoiler
原本書上html是寫下面這樣
```html
<div id="test1">
  <p>總金額共{{ subtotalComputed }}元</p>
  <p>總金額共{{ subtotalMethods() }}元</p>
</div>
```
搭配以下的js，在最後更新 data 裡的 message 屬性
```javascript
const vm = createApp({
  data() {
    return {
      message: "Hello Vue!",
    };
  },
  computed: {
    subtotalComputed: function () {
      console.log("computed");
      return 100 * 100;
    },
  },
  methods: {
    subtotalMethods() {
      console.log("methods");
      return 100 * 100;
    },
  },
}).mount("#test1");

vm.$data.message = "Hey";
```
但是都只有呼叫一次而已
![image](https://hackmd.io/_uploads/H10LX9Z7yl.png)

所以我把html加上一行要取用message的地方
```html
<div id="test1">
  <p>總金額共{{ subtotalComputed }}元</p>
  <p>總金額共{{ subtotalMethods() }}元</p>
  {{ message }}
</div>
```
就可以看到methods被執行了兩次
![image](https://hackmd.io/_uploads/rkriX9ZX1x.png)
:::

### computed / methods 的使用時機比較
幣值轉換
```html
<!-- 使用methods -->
<!-- 要綁v-on，雖然現在還看不懂v-on是啥 -->
<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>

<!-- 使用computed -->
<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>
```
>之後會提到 v-model 是幹嘛的
>各位讀者只要知道這裡放上 v-model ，使用者輸入的內容會跑進 Vue 實體對應的屬性即可。
```javascript
// 使用methods，每一種轉換就要寫一個function
const vm = Vue.createApp({
  data () { // 每一個幣值就要記一個
    return { 
      twd: 0.278,
      jpy: 1,
    }
  },
  methods: { // 要寫兩個function
    twd2jpy () {
    	this.jpy = Number.parseFloat(Number(this.twd) / 0.278).toFixed(3);
    },
    jpy2twd () {
    	this.twd = Number.parseFloat(Number(this.jpy) * 0.278).toFixed(3);
    },
  },
  mounted () { // 這個component掛載的時候會執行的function
  	this.twd2jpy();
  }
}).mount('#app');

// 使用computed，重點是從台幣轉換成各種幣值，也就是以台幣為基準點來轉換
const vm = Vue.createApp({
  data () { // 紀錄台幣就好
    return {
      twd: 0.278 // 這個型別是string
    }
  },
  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');
```
#### 自行測試


把各種幣別統一管理在methods裡面，再用`this.XXX`取用這個method

```javascript
methods: {
  getExchangeRate(currency) {
    let rate = 1;
    switch (currency) {
      case "jpy":
        rate = 0.22;
        break;
      case "usd":
        rate = 31.5;
        break;
    }
    return rate;
  },
},
computed: {
  jpy: {
    get() {
      return Number(Number(this.twd) / this.getExchangeRate("jpy")).toFixed(2);
    },
    set(value) {
      this.twd = Number(Number(value) * this.getExchangeRate("jpy")).toFixed(2);
    },
  },
  usd: {
    get() {
      return Number(Number(this.twd) / this.getExchangeRate("usd")).toFixed(2);
    },
    set(value) {
      this.twd = Number(Number(value) * this.getExchangeRate("usd")).toFixed(2);
    },
  },
},
```
:question:在輸入時會一直強制變成小數點兩位的狀態

## 1-4 Vue.js 的黑魔法: 指令
### 屬性綁定 - `v-bind`
屬性也用變數的方式去渲染
```javascript
const vm = createApp({
  data() {
    return {
      isDisabled: true,
    };
  },
}).mount(#app);
```
```html
<button v-bind:disabled="isDisabled">click me!</button>
```
渲染出來就是`disabled`這個屬性是`isDisabled`屬性的值`true`
![image](https://hackmd.io/_uploads/B1xlDT-7yg.png)

### 表單綁定 - `v-model`
要是表單元素才可以透過`v-model`進行雙向綁定，例如 `<input>`、`<textarea>` 以及 `<select>`
雙向綁定理解：輸入的值會設定成`v-model`的變數的值。

#### `<input type="radio">`
原本同一組radio要使用同一個`name`才能綁在一起
```html
<div>
    <input
      type="radio"
      name="contact"
      id="contactChoice1"
      value="email"
    />
    <label for="contactChoice1">Email</label>
    <input
      type="radio"
      name="contact"
      id="contactChoice2"
      value="phone"
    />
    <label for="contactChoice2">Phone</label>
    <input
      type="radio"
      name="contact"
      id="contactChoice3"
      value="line"
    />
    <label for="contactChoice3">Line</label>
</div>
```
現在可以設定同一個`v-model`，讓radio綁成一組
```html
<div>
  <input
    type="radio"
    id="contactChoice1"
    value="email"
    v-model="contact"
  />
  <label for="contactChoice1">Email</label>
  <input
    type="radio"
    id="contactChoice2"
    value="phone"
    v-model="contact"
  />
  <label for="contactChoice2">Phone</label>
  <input
    type="radio"
    id="contactChoice3"
    value="line"
    v-model="contact"
  />
  <label for="contactChoice3">Line</label>
</div>
```
並且在實體中設定，就會預設選擇設定的那個value
```javascript
const vm = createApp({
  data() {
    return {
      contact: "line",
    };
  },
}).mount("#app");
```
![image](https://hackmd.io/_uploads/ryQLaH2Qkl.png)

#### 自己亂測試
設定成同一個`v-model`之後，即使`name`不一樣也會綁在一起
設定成同一個`name`，即使`v-model`不一樣也會綁在一起

#### `<input type="checkbox">`
和radio的差別是實體中設定要是陣列，如果不是陣列就會全選

#### `<input type="select">`
他的範例壞掉惹，但程式碼是對的

把「請選擇」的value=""刪掉好像也沒關係
```html
<select v-model="selected">
  <option disabled>請選擇</option>
  <option>台北市</option>
  <option>新北市</option>
  <option>基隆市</option>
</select>

<p>Selected: {{ selected }}</p>
```
讓一開始選項停在「請選擇」
```javascript
const vm = createApp({
  data() {
    return {
      selected: "請選擇",
    };
  },
}).mount("#app");
```
### v-model 與修飾子
#### `.lazy`
從監聽input事件改為change事件，當使用者離開輸入框焦點才會觸發事件

#### `.number`
input輸入的型別從字串改為數字，如果想取得的是數字就可以自動幫忙轉型並作運算

#### `.trim`
自動刪除輸入前後的空白

### 模板綁定 - v-text / v-html / v-once / v-pre
#### `v-text`
跟{{ }}有類似的效果，但設定在標籤上的話就會把這個標籤的文字(我猜是text-content)(書上寫innerHTML)換成變數的內容

#### `v-html`
把標籤的innerHTML換成變數的內容

#### `v-once`
只會渲染一次

#### `v-pre`
不使用指令
如果想要顯示{{ }}大括號本身的話就要用這個指令
`<div v-html="rawContent" v-pre></div>` 這樣也不會顯示rawContent的值

### v-bind 樣式綁定 class/style
```css
.error {
  color: red;
}
```

綁class`error`，語法會判斷屬性(error)的值(isError)的truthiness，是true就加上這個屬性成為class
[官方文件](https://vuejs.org/guide/essentials/class-and-style.html)
```html
<input type="text" v-model.trim="title" v-bind:class="{error: isError}" />

<input
  type="text"
  v-model.trim="title"
  v-bind:class="{ error: title.length > 10 }"
/>
```

把邏輯移到computed
```javascript
computed: {
  isError: function () {
    return this.title.length > 10;
  },
},  
```

style中的css推薦使用駝峰式，如果用烤肉串式要用引號括起來
```html
<input
  type="text"
  v-model.trim="title"
  v-bind:style="{ fontSize: fontSize }"
/>
```
```javascript
data() {
  return {
    fontSize: "30px",
  };
},
```
放進array，例如跟瀏覽器支援相關的，會使用最後一個瀏覽器有支援的
```html
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
```