[Vue] Options API
TL;DR
Vue3提供Options API 跟新增的 Composition API
初學者學習Options API比較好,因為目前專案仍大多是使用Options API,且Vue2也是使用Options API。
在使用Options API時會頻繁使用this
在使用箭頭函式時要特別注意
參考資料
- Vue 筆記 - Computed 的 get() 與 set()(TimCodingBlog) | 提姆寫程式
- Vue 語法補充介紹(computed, watch…)(Jason) | Medium
相關連結
methods
觸發方式
methods為一個物件,裡面存在許多的函式。調用methods內函式有許多的方法。
可以由v-on
觸發,由其他methods內的方法觸發,也可以由生命週期觸發。
- html(#app)
- vue
<div id="app">
<div class="p-5">
<h3>methods 的結構</h3>
<ul>
<li>由 methods 定義的物件</li>
<li>內層均是函式</li>
</ul>
<h3>methods 的觸發方法(點擊、其它 options api、 生命週期...)</h3>
<button type="button" @click="trigger('Click Methods')">點擊觸發</button>
<button type="button" @click="callOtherMethod" class="ms-1">呼叫另一個 methods</button>
</div>
</div>
const App = {
data() {
return {
}
},
methods: {
trigger(name) {
console.log(name, '此事件被觸發了')
},
callOtherMethod() {
this.trigger('由 callOtherMethod 觸發')
},
methodParameter(a, b, c, d) {
console.log(a, b, c, d)
},
},
created() {
this.trigger('由生命週期觸發');
}
};
Vue.createApp(App).mount('#app');
傳入參數
當使用v-on
指令觸發methods時,我們可以使用一個特殊的變數$event
,將事件物件傳入方法中。
<button @click="functionName('參數一',$event)">傳入$event</button>
如果這個function只有一個變數會傳入,則在使用v-on觸發時不帶小括號,該function的變數會自動為事件物件
<button @click="functionName">點擊會自動傳入事件物件</button>
處理複雜資料
- html(#app)
- vue
<div id="app">
<div class="p-5">
<div id="app">
<h3>使用 methods 處理複雜資料</h3>
<ul>
<li v-for="product in products">
{{ product.name }} / {{ product.price }}
<button type="button" @click="addToCart(product)">加入購物車</button>
</li>
</ul>
<h6>購物車項目</h6>
<ul>
<li v-for="item in carts">{{ item.name }}</li>
</ul>
總金額 {{ sum }}
<h3>作為 $filter 使用(取代複雜表達式)</h3>
總金額 {{ convertToAmount(sum) }}
</div>
</div>
</div>
const App = {
data() {
return {
products: [
{
name: '蛋餅',
price: 30,
vegan: false
},
{
name: '飯糰',
price: 35,
vegan: false
},
{
name: '小籠包',
price: 60,
vegan: false
},
{
name: '蘿蔔糕',
price: 30,
vegan: true
},
],
carts: [],
sum: 0,
}
},
methods: {
addToCart(product) {
this.carts.push(product)
this.calculate();
},
calculate() {
let total = 0;
this.carts.forEach(item => {
total += item.price;
});
this.sum = total;
},
convertToAmount(price) {
return `NT$ ${price}`;
}
},
created() {}
};
Vue.createApp(App).mount('#app');
Line:6 ~ Line:9 使用v-for
渲染出所有選項,並且在每個選項按鈕上綁定相對應的@click
,將該項目的product物件傳入addToCart
函式中。
Line:13 中我們事先準備好一個li
,同樣使用v-for
迴圈渲染出購物車的項目。每當購物車carts內的內容有新增,就會渲染到畫面上。
在 Line:15中,我們直接在畫面上渲染{{ sum }}
的值。這個代表的是所有購物車內的價格計算加總出來的結果。
但是我們需要觸發讓他可以計算出對應的價格,並且存入data
的sum
內。
這邊我們在每次點擊按鈕新增資料到購物車內時,調用計算價格的函式,並且存入資料中。
const App = {
data() {
return {
products: [
{
name: '蛋餅',
price: 30,
vegan: false
},
...
],
carts: [],
sum: 0,
}
},
methods: {
addToCart(product) {
this.carts.push(product)
this.calculate();
},
calculate() {
let total = 0;
this.carts.forEach(item => {
total += item.price;
});
this.sum = total;
},
...
},
created() {}
};
Vue.createApp(App).mount('#app');
以前在vue2時有一個filter 的 API,現在可以直接使用methods取代。
在methods中撰寫一個function:
convertToAmount(price) {
return `NT$ ${price}`;
}
該方法會回傳一個組合好的字串
並且在畫面上直接在{{}}
內插入表達式,去調用這個function,將data內的num值給傳入
總金額 {{ convertToAmount(sum) }}
結果就會顯示出
總金額 NT$ 125
computed
語法
computed是為了簡化在渲染時有過多的邏輯。computed為一個物件,物件內宣告的函式名稱即為調用名稱。調用時的值為函式內的回傳值。
在使用computed API的時候,要避免在函式內去直接更動資料(但是仍然有提供setter的方式,詳見後方),也不要有非同步的行為在內。
另外在computed內具有快取的機制,只要資料本身沒有變動,則computed所回傳出來的內容都會直接取用快取的值。
- html(#app)
- vue
<div id="app">
<div class="p-5">
{{foo}} / {{typeof foo}}
<!-- 100 / string -->
</div>
</div>
const App={
data(){
},
methods:{
},
computed:{
foo(){
// 這邊簡單回傳一個沒有經過計算的純字串100,實際使用時可以 經過複雜運算之後再回傳
return '100'
}
}
}
Vue.createApp(App).mount('#app');
使用範例
前面有使用methods
計算出total的值,這邊改用computed
去改寫:
- html(#app)
- vue
<div id="app">
<div class="p-5">
<h3>Computed 將目前的數值運算呈現至畫面上</h3>
<ul>
<li v-for="product in products">
{{ product.name }} / {{ product.price }}
<button type="button" @click="addToCart(product)">加入購物車</button>
</li>
</ul>
...
<h6>購物車項目</h6>
<ul>
<li v-for="item in carts">{{ item.name }}</li>
</ul>
total 的值:{{total}} <br>
</div>
</div>
const App = {
data() {
return {
products: [
{
name: '蛋餅',
price: 30,
vegan: false
},
{
name: '飯糰',
price: 35,
vegan: false
},
{
name: '小籠包',
price: 60,
vegan: false
},
{
name: '蘿蔔糕',
price: 30,
vegan: true
},
],
carts: [],
sum: 0,
}
},
methods: {
addToCart(product) {
this.carts.push(product)
},
},
computed: {
total() {
let total = 0;
this.carts.forEach(item => {
total += item.price;
});
return total;
}
}
};
Vue.createApp(App).mount('#app');
也可以在return的時候使用樣板字面值:
const App = {
data() {
return {...}
},
methods: {...},
computed: {
total() {
let total = 0;
this.carts.forEach(item => {
total += item.price;
});
return `總共 NTD:${total}元`;
}
}
};
Vue.createApp(App).mount('#app');
搜尋應用
- html(#app)
- vue
<div id="app">
<div class="p-5">
<h3>Computed 常見技巧 - 搜尋</h3>
<input type="search" v-model="search">
<ul>
<li v-for="item in filterProducts">
{{ item.name }} / {{ item.price }}
</li>
</ul>
<hr>
<p v-pre>{{filterProducts}}的內容</p>
{{filterProducts}}
</div>
</div>
const App = {
data() {
return {
search:'',
products: [
{
name: '蛋餅',
price: 30,
vegan: false
},
{
name: '飯糰',
price: 35,
vegan: false
},
{
name: '小籠包',
price: 60,
vegan: false
},
{
name: '蘿蔔糕',
price: 30,
vegan: true
},
],
carts: [],
sum: 0,
}
},
methods: {
addToCart(product) {
this.carts.push(product)
},
},
computed: {
total() {
let total = 0;
this.carts.forEach(item => {
total += item.price;
});
return total;
},
filterProducts(){
return this.products.filter(item=>{
return item.name.match(this.search)
})
}
}
};
Vue.createApp(App).mount('#app');
在computed內定義一個filterProducts
的函式,該函式的回傳值會是與search
資料比對後回傳的陣列。
因為search
的值與input透過v-model
雙向綁定,所以filterProducts
所回傳的陣列可以動態改變。
因為filterProducts
回傳的是陣列,所以可以使用v-for
來回圈渲染所需要的內容。
getter/setter
前面所介紹的其實是computed的getter
用法
computed另外還有setter
的用法。
getter
要啟用computed的setter
,首先必須先將函式改為物件,並且在裡面加上get()
跟set()
:
const App = {
data() {
return {...}
},
methods: {...},
computed: {
total: {
get() {
let total = 0;
this.carts.forEach(item => {
total += item.price;
});
return total;
},
set(val) {
...
}
},
},
};
Vue.createApp(App).mount('#app');
totla在函式寫法時,回傳的值即為getter的值。
我們將原本的內容調整到total物件內的get函式中,運作會目前會完全相同。
setter
setter顧名思義,與getter是相反的。
getter會將資料取出,運算後渲染到畫面上。而setter則是可以將資料運算後再存回。
- html(#app)
- vue
<div id="app">
<div class="p-5">
<ul>
<li v-for="product in products">
{{ product.name }} / {{ product.price }}
<button type="button" @click="addToCart(product)">加入購物車</button>
</li>
</ul>
...
<h6>購物車項目</h6>
<ul>
<li v-for="item in carts">{{ item.name }}</li>
</ul>
total 的值:{{total}} <br>
<h3>Computed Getter, Setter</h3>
sum 的值:
<input type="number" v-model.number="num">
<button type="button" @click="total = num">更新</button>
total 的值:{{ total }}<br>
sum 的值:{{ sum }}
</div>
</div>
const App = {
data() {
return {
num: 0,
search: '',
products: [
{...}, {...}
],
carts: [],
sum: 0,
}
},
methods: {
addToCart(product) {
this.carts.push(product)
},
},
computed: {
total: {
get() {
let total = 0;
this.carts.forEach(item => {
total += item.price;
});
// 這邊改成如果sum有值,則以sum為主;sum沒有值則會以計算的total為主。
return this.sum || total;
},
set(val) {
// 透過事件將input內的值(num)傳到total setter的val上,並且存回data內的sum
this.sum=val;
}
},
}
};
Vue.createApp(App).mount('#app');
watch
watch
跟computed
的差異在於:
監聽變數的數量
- computed可以同時監聽多個變數
- watch只能監聽單一個變數
反應結果的方式不同
- computed利用return直接回傳運算結果
- watch透過this.data寫入資料反應運算結果
初始化時是否會運算結果
- computed在初始化時就會有一個return的值
- watch必須要等到第一次資料有變動才會執行裡面的運算
如果需要監聽多個變數並計算出一個結果,需使用computed
只需監聽一個變數但是要動態調整多筆資料則是使用watch
語法
watch與computed相同,為一個物件,物件內所宣告的函示名稱即為欲監聽的資料變數名稱。該函示可以帶入兩個參數,分別為變動過後的值與變動前的值
- html(#app)
- vue
<div id="app">
<div class="p-5">
<input type="number" v-model.lazy.number="foo">
{{foo}} / {{bar}}
</div>
</div>
const App={
data(){
return {
// 要監聽foo
foo:0,
// 每次更動foo之後會觸發更新bar的內容
bar:''
}
},
watch:{
// 函示名稱要與data變數名稱相同,n代表新值new;o代表舊值old
foo(n,o){
this.bar=`bar現在的值是${n},先前的值是${o}`
}
}
}
Vue.createApp(App).mount('#app');
使用範例
- html(#app)
- vue
<div id="app">
<div class="p-5">
<h3>watch 監聽單一變數</h3>
<label for="name">名字須超過十個字</label>
<input type="text" id="name" v-model="tempName">
<P>result: {{ result }}</P>
<p>name: {{ name }}</p>
<label for="productName">商品名稱</label><input type="text" v-model="productName"><br>
<label for="productPrice">商品價格</label><input type="number" v-model.number="productPrice"><br>
<label><input type="checkbox" v-model="productVegan"> 素食</label>
<p>Computed result2: {{ result2 }}</p>
<p>Watch result3: {{ result3 }}</p>
<h3>watch 深層監聽</h3>
<label for="productName">商品名稱</label><input type="text" v-model="product.name"><br>
<label for="productPrice">商品價格</label><input type="number" v-model.number="product.price"><br>
<label><input type="checkbox" v-model="product.vegan"> 素食</label>
<p>result4: {{ result4 }}</p>
</div>
</div>
const App = {
data() {
return {
name: '',
tempName: '',
result: '',
result3: '',
result4: '',
// 單一產品獨立監聽
productName: '蛋餅',
productPrice: 30,
productVegan: false,
}
},
computed: {
result2() {
return `媽媽買了 ${this.productName},總共花費 ${this.productPrice} 元,另外這 ${this.productVegan ? '是' : '不是'} 素食的`;
},
},
watch: {
tempName(n, o) {
if (n.length >= 10) {
this.result = `文字長度為 ${n.length} 個字,將儲存至變數中`;
this.name = n;
} else {
this.result = `輸入的文字僅有 ${n.length} 個字,上一次有 ${o.length} 個字`;
}
},
productName(){
this.result3= `媽媽買了 ${this.productName},總共花費 ${this.productPrice} 元,另外這 ${this.productVegan ? '是' : '不是'} 素食的`;
},
productPrice(){
this.result3= `媽媽買了 ${this.productName},總共花費 ${this.productPrice} 元,另外這 ${this.productVegan ? '是' : '不是'} 素食的`;
},
productVegan(){
this.result3= `媽媽買了 ${this.productName},總共花費 ${this.productPrice} 元,另外這 ${this.productVegan ? '是' : '不是'} 素食的`;
},
}
};
Vue.createApp(App).mount('#app');
在vue中的 Line:15 ~ Line:19 中,computed可以一次監聽productName
,productPrice
,productVegan
三個變數。但是只能回傳出一個字串並賦予到result2
中。
而vue中的 Line:21 ~ Line:28 雖然只能監聽tempName
一個變數,但是根據監聽變數的變化,同時操作this.result
跟this.name
兩個變數。
深層監聽
如果資料為物件的形式,希望在該物件內有任何資料變動都會即時反應,則可以使用深層監聽。
- html(#app)
- vue
<div id="app">
<div class="p-5">
<h3>watch 深層監聽</h3>
<label for="productName">商品名稱</label><input type="text" v-model="product.name"><br>
<label for="productPrice">商品價格</label><input type="number" v-model.number="product.price"><br>
<label><input type="checkbox" v-model="product.vegan"> 素食</label>
<p>result4: {{ result4 }}</p>
</div>
</div>
const App = {
data() {
return {
result4: '',
// 單一產品深層監聽,必須為物件
product: {
name: '蛋餅',
price: 30,
vegan: false
}
}
},
watch: {
product:{
handler(n,o){
// console.log(n,o)
this.result4=`媽媽買了 ${this.product.name},總共花費 ${this.product.price} 元,另外這 ${this.product.vegan ? '是' : '不是'} 素食的`;
},
deep:true
}
}
};
Vue.createApp(App).mount('#app');
其中,以下格式為固定的結構:
watch:{
watchedObjectName:{
handler(n,o){
...
},
deep:true
}
}
只要watchedObjectName內的任何資料有變動,都會觸發這個watch API。
官方文件內有提到,深層監聽的物件中newValue跟oldValue會是相等的。這是因為他們是同一個對象(call by reference)
因為當深層監聽的對象是物件或陣列時,Vue 是根據 Reference(記憶體位置)來判斷是否相同
而觸發監聽的時候,監聽對象的 Reference 不會改變,所以新值與舊值都會是同一個物件
另外也有說明要謹慎使用深層監聽,因為會需要花費許多的資源(去監聽裡面的每一個屬性)