# 1-6 條件判斷與列表渲染
終於來到指令系列的最後一篇。
在這個小節中,會為各位讀者介紹如何透過 Vue.js 的 v-if
/ v-show
等指令來對畫面的元素進行條件與流程的控制,
以及使用 v-for
指令來對陣列或物件類型的資料進行迭代渲染。
# 條件判斷 v-if
/ v-show
如果要顯示或隱藏模板中的元素, Vue.js 提供了兩個指令來幫助我們完成任務,
分別是 v-show
與 v-if
。 兩者的使用方式大同小異:
<div v-if="isShow">v-if</div>
<div v-show="isShow">v-show</div>
2
3
就這個範例上來說,這兩者看起來作用似乎一樣,不過實際上它們還是有些不同的地方。
# v-show
v-show
作用十分單純,就是值為 truthy
就顯示, falsy
則隱藏。
注意,這裡說的是「隱藏」而非移除元素。
讀者不妨開啟瀏覽器的 devtools 來觀察,當 v-if
對應的值為 falsy
時,元素會直接被移除,
而在 v-show
的情況,則是透過 CSS 的 display: none
來將元素隱藏。
# v-if
前面說過 v-if
雖然與 v-show
同樣都是控制元素是否出現在畫面,
但除了元素的增減之外,v-if
也如同我們所熟知的程式語法般,v-if
還可以與 v-else-if
與 v-else
來與不同條件做搭配:
<div v-if="count === 0">Block A</div>
<div v-else-if="count < 5">Block B</div>
<div v-else>Block C</div>
2
3
像這樣,我們可以針對不同的條件設定 v-if
、 v-else-if
與 v-else
來判斷何時顯示對應的元素:
另外,若是有多個元素希望同時根據某個條件來切換時,如:
<!-- value === 'A' 時出現 -->
<h1>Title A</h1>
<p>Paragraph A - 1</p>
<p>Paragraph A - 2</p>
<!-- value 不為 A 時出現 -->
<h1>Title B</h1>
<p>Paragraph B - 1</p>
<p>Paragraph B - 2</p>
2
3
4
5
6
7
8
9
若是寫成:
<!-- value === 'A' 時出現 -->
<h1 v-if="value === 'A'">Title A</h1>
<p v-if="value === 'A'">Paragraph A - 1</p>
<p v-if="value === 'A'">Paragraph A - 2</p>
<!-- value 不為 A 時出現 -->
<h1 v-else>Title B</h1>
<p v-else>Paragraph B - 1</p>
<p v-else>Paragraph B - 2</p>
2
3
4
5
6
7
8
9
像這樣,由於連續三個元素都出現 v-else
指令,便會造成 Vue.js 報錯。
當然也可以針對每個標籤寫上 v-if="value !== 'A'"
,但又顯得囉唆。
此時若是不想在外層多包一個 <div>
,則可以透過 <template>
標籤來包覆。
<!-- value === 'A' 時出現 -->
<template v-if="value === 'A'">
<h1>Title A</h1>
<p>Paragraph A - 1</p>
<p>Paragraph A - 2</p>
</template>
<!-- value 不為 A 時出現 -->
<template v-else>
<h1>Title B</h1>
<p>Paragraph B - 1</p>
<p>Paragraph B - 2</p>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
像這樣, Vue.js 在模板編譯後並不會將 <template>
渲染至畫面上,也仍保有 v-if
指令的作用。
# v-if
與 key
屬性
Vue.js 為了提高網頁渲染的效率,在預設情況下會選擇重複利用已經存在的元素而不是重新渲染。 這樣的做法確實能提高效能,不過在某些場景下可能會出現問題。
像是過去在 Vue 2.x 的時候,我們在 <template>
裡面加上 v-if
與 <input>
:
<!-- for Vue 2.x -->
<template v-if="loginType === 'username'">
<label>Username</label>
<input placeholder="Enter your username">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email address">
</template>
2
3
4
5
6
7
8
9
10
這個時候可以試著在輸入框隨意輸入幾個字,然後在 「Username」 與 「Email」 選項切換,
此時你會發現雖然在模板是兩組不同的 input
輸入框,但切換選項後的文字卻被保留了下來。
若我們想排除這個問題,只需要針對不同的表單元素加上 key
來表示:
<template v-if="loginType === 'username'">
<label>Username</label>
<input placeholder="Enter your username" key="username">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email address" key="email">
</template>
2
3
4
5
6
7
8
9
Vue.js 的 v-if
會根據 key
屬性的內容是否相同來決定是否重新渲染元素,
若是透過 v-show
指令來切換,則無須加上 key
屬性,因為兩個元素一直都存在,沒有重新渲染的問題。
換句話說,若對應的指定的 DOM (或元件) 的狀態需要被保留,且狀態可能頻繁更動時,用 v-show
會比 v-if
更適合,
且由於 DOM 不會被動態增加或刪減,執行時的效能更好。
另一方面,當條件判斷式的結果幾乎不變的時候,則建議使用 v-if
。
像是用來判斷使用者登入與否的區塊,這種情況下,更理想的做法是減少元素的數量來降低瀏覽器渲染成本,無須將不相關的 DOM 節點全部生成。
小提醒
這個問題在 Vue 3.0 之後已經被修正,所以在使用 Vue 3.0 的情況下, <input>
可以不必加上 key
屬性。
# v-for
列表渲染
在 Vue.js 裡頭,可透過 v-for
指令在模板中,將「陣列」或「物件」類型的資料進行迭代渲染 (重複呈現在畫面上)。
# v-for
與陣列
v-for
指令會以 item in items
的語法形式來使用,如:
const vm = Vue.createApp({
data () {
return {
arr: ['008', 'JS', 'is', 'awesome']
}
}
}).mount('#app');
2
3
4
5
6
7
<div id="app">
<ul>
<li v-for="item in arr">{{ item }}</li>
</ul>
</div>
2
3
4
5
這裡的 item
的名稱可任意命名,只要是合法的 JavaScript 變數名稱即可。
我們也可以在 v-for
指令加入索引值 index
,如:
<ul>
<li v-for="(item, index) in arr">{{ index }} / {{ item }}</li>
</ul>
2
3
注意
注意,v-for
列表的索引值與陣列一樣,是從 0
開始計算的。
# v-for
與物件
v-for
也可以用來遍歷物件形式的資料:
const vm = Vue.createApp({
data () {
return {
book: {
title: '08js',
author: 'Kuro',
publishedAt: '2019/09'
}
}
}
}).mount('#app');
2
3
4
5
6
7
8
9
10
11
<div id="app">
<ul>
<li v-for="val in book">{{ val }}</li>
</ul>
</div>
2
3
4
5
當然,與陣列一樣,我們也可以將物件的 key
值一並列出:
<div id="app">
<ul>
<li v-for="(val, key) in book">{{ key }} / {{ val }}</li>
</ul>
</div>
2
3
4
5
甚至是索引值:
<div id="app">
<ul>
<li v-for="(val, key, index) in book">
{{ index }} / {{ key }} / {{ val }}
</li>
</ul>
</div>
2
3
4
5
6
7
小提醒
由於物件 (object) 在 JavaScript 本身並不帶有順序的特性,物件屬性間的順序會依照瀏覽器對 Object.keys()
實作方式的不同而有所差異,
因此 v-for
在不同瀏覽器環境下,渲染物件的列表順序可能會有所不同。
如果要確保渲染順序,可先將物件轉換為陣列後再進行排序。
# v-for
與範圍
除了陣列與物件之外, v-for
也可以接受整數作為渲染的條件,數字越大重複的次數越多。
<ul class="pagination">
<li class="page-item"><a class="page-link" href><</a></li>
<!-- 表示出現十次 -->
<li class="page-item" v-for="page in 10">
<a class="page-link" href>{{ page }}</a>
</li>
<li class="page-item"><a class="page-link"href>></a></li>
</ul>
2
3
4
5
6
7
8
在這個範例中,我們就可以透過 v-for="page in 10"
來做到渲染頁碼的效果。
# v-for
與 <template>
若我們有多個節點希望同時透過 v-for
來循環渲染,則可以透過包覆一層 <template>
來搭配使用,
例如 Bootstrap 裡 dropdown-menu (opens new window) 的分隔線:
<div class="dropdown-menu">
<template v-for="i in links">
<div class="dropdown-divider"></div>
<a class="dropdown-item" :href="i.link">i.title</a>
</template>
</div>
2
3
4
5
6
像這樣,每一個透過 v-for
渲染出的 dropdown-item
都會有分隔線,而且這個 <template>
標籤與使用 v-if
一樣不會出現在渲染後的網頁內容。
# v-for
列表的排序與過濾
有時候我們希望針對 v-for
渲染的列表來進行排序或搜尋過濾,但是很遺憾地告訴各位,v-for
指令內建並未提供這類功能。 (完)
開玩笑的,雖然 v-for
指令沒有提供這類功能,但我們可以透過前面章節介紹過的 computed
或 methods
屬性來產生對應的新陣列。
如:
data () {
return {
numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
}
},
computed: {
// 只顯示偶數
evenNumbers () {
return this.numbers.filter(number => number % 2 === 0);
}
}
2
3
4
5
6
7
8
9
10
11
<div class="block" v-for="i in evenNumbers">{{ i }}</div>
由於 Array.filter 會回傳一個新的陣列,所以沒問題。
但若是在進行陣列排序的時候要小心,由於 JavaScript 的 sort
會直接影響原始陣列:
computed: {
sortedNumbers () {
// 注意! 若在此直接執行 .sort 會直接改變 this.numbers 的順序!
return this.numbers.sort((a, b) => b - a);
}
}
2
3
4
5
6
比較好的做法是先複製一份新的陣列再進行排序,如:
computed: {
sortedNumbers() {
// 先透過 [...this.numbers] 複製一份新陣列再排序
return [...this.numbers].sort((a, b) => b - a);
}
}
2
3
4
5
6
這樣就不會影響原有陣列內容了。
# v-for
可以使用 index
當作 key
嗎?
在理解 index
可否作為 key
之前,先來介紹 key
在 v-for
的作用。
與前面介紹過的 v-if
一樣,v-for
為了提高網頁渲染的效率,會選擇重複利用已經存在的元素而不是重新渲染。
換句話說,當陣列的順序被改變時, Vue.js 不會移動實際 DOM 的節點,而是更新現有的 DOM 內容。
但是當 v-for
內部含有子元件或表單元素的時候,這個時候要是沒有加上 key
屬性,就可能會出現一些不可預期的錯誤。
舉個例子,這裡有個 Todo list 的列表,我們期望當這份列表內的 checkbox
被勾起時,
會自動把選項移動到 Done List 的列表:
<!-- Todo list -->
<h1>Todo</h1>
<ul>
<li v-for="i in todoLists">
<label><input v-model="i.isDone" type="checkbox">{{ i.title }}</label>
</li>
</ul>
<!-- Done List -->
<h1>Todo</h1>
<ul>
<li v-for="i in doneLists">
<label><input v-model="i.isDone" type="checkbox">{{ i.title }}</label>
</li>
</ul>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data () {
return {
lists: [
{
id: 'task001',
title: '選項 1',
isDone: false
},
{
id: 'task002',
title: '選項 2',
isDone: false
},
{
id: 'task003',
title: '選項 3',
isDone: false
},
]
}
},
computed: {
todoLists () {
return this.lists.filter( d => !d.isDone )
},
doneLists () {
return this.lists.filter( d => d.isDone )
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
這是在 v-for
沒有加上 key
屬性的範例。
當我們勾選「選項 3」的時候,程式的作用是正常的:
但是,如果我們勾選「選項 2」的時候,問題就來了, 此時會發現 Todo List 的「選項 2」確實如預期般地跑到 Done List 列表, 但是這個時候 Todo List 的「選項 3」也被勾起來了。
這是為什麼呢?
剛剛我們說過, Vue.js 做到為了效能最佳化,會把已經渲染至畫面的 DOM 節點拿來重複利用。
當我們打勾的選項是最後一個的時候,因為 todo list
變動的元素剛好是最後一個,所以沒有問題。
然而,當我們打勾的是「選項 2」的時候,這個時候 done list
會多出一個選項 2 。
但對 todo list
來說,長度減一,所以對應的元素少了一個,
原本在「選項 2」的文字更新,但 checkbox
卻在這個時候被拿來重複利用:
於是就會出現明明只勾選了一個「選項 2」,卻有兩個選項都被打勾的錯誤現象。
若是要解決這個問題,只要加個「唯一的」 key
屬性作為識別,即可確保畫面的重新渲染:
<!-- todoLists -->
<li v-for="i in todoLists" :key="i.id">
<label><input v-model="i.isDone" type="checkbox"> {{ i.title }}</label>
</li>
<!-- doneLists -->
<li v-for="i in doneLists" :key="i.id">
<label><input v-model="i.isDone" type="checkbox"> {{ i.title }}</label>
</li>
2
3
4
5
6
7
8
9
小提醒
:key
為 v-bind:key
之簡寫。
那麼,回到本小節的標題, v-for
裡的 index
是否可以當作 key
來使用呢?
相信讀者看到這裡應該也能得出一樣的答案:不適合。
由於 v-for
裡的 index
是隨著陣列而生成的,換句話說,當 index
沒變的時候,對 Vue.js 而言,它就是一個可以重複使用的元素 (或元件)。
<!-- 將 index 設定為 key -->
<li v-for="(i, index) in todoLists" :key="index">
<label><input v-model="i.isDone" type="checkbox"> {{ i.title }}</label>
</li>
2
3
4
這個時候,即使我們為 v-for
加上了 key
屬性,它的作用也會跟沒加一樣。
小提醒
想一想,在 v-for
的列表中,加上 key
屬性與沒加上 key
屬性,哪種執行的效率高?
# [補充] 當陣列的內容變動,畫面為何沒更新? (Vue 2.x)
在本篇的最後,來談談很多新手在使用 Vue 2.x 會遇到的問題,明明陣列的內容已經變動,畫面為何沒更新?
先來看個簡單的範例:
<ul>
<li v-for="n in items">{{ n }}</li>
</ul>
<button @click="changeVal">click</button>
2
3
4
5
// for Vue 2.x
const vm = new Vue({
data () {
return {
items: ["Vue", "is", "Awesome"]
}
},
methods: {
changeVal () {
this.items[0] = '08JS';
// 加上 alert 確定陣列確實已被更新
// 但是模板內 list 的內容不會更新
alert(this.items);
}
}
}).$mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
這就要提到 Vue.js 狀態與畫面的更新機制 (Vue 2.x) - Object.defineProperty()
。
Vue.js 2.x 在建立實體的時候,會針對 data
裡的屬性增加 getter
與 setter
,
其實背後原理也就是 Object.defineProperty
的 get
與 set
function。
let title = '08JS好棒棒';
const book = {};
Object.defineProperty(book, 'title', {
set: val => {
title = val;
console.log('我被更新了!', val);
},
get: () => {
return title;
},
});
// '08JS好棒棒'
console.log(book.title);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
像這樣,雖然我們沒有針對 book
物件設定 title
屬性,
但是當我們試著去取得 book.title
的時候,就會透過 Object.defineProperty
的 get
function 來拿到對應的值。
而當我們嘗試去修改 book.title
的時候,實際上是透過 Object.defineProperty
的 set
function 來更新對應的內容:
// 延續前一範例
book.title = 'Vue.js好棒棒';
// console 主控台會印出:
// 我被更新了! Vue.js好棒棒
2
3
4
5
這也是為什麼 Vue.js 能夠即時偵測狀態的更新的原理。
繼續前面範例,如果此時,我們將 book.title
指定為陣列,並且更新內容:
// 會觸發 setter function,印出 "我被更新了! [1, 2, 3, 4, 5]"
book.title = [1, 2, 3, 4, 5]
// 由於 book.title 並未重新指定 (re-assigned)
// 所以不會觸發 setter function,但是陣列內容被更新了
book.title[0] = '重新認識 JavaScript 好棒棒';
// 不會觸發 setter function
book.title.push('重新認識 Vue.js 也好棒棒');
2
3
4
5
6
7
8
9
此時我們發現,只要 book.title
在沒有被重新賦值的情況下, setter
是不會被觸發的。
Vue.js 的開發團隊當然也發現了這樣的問題,所以他們針對了 JavaScript 陣列的幾種方法進行包裝改寫:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
這裡節錄部分 Vue.js 2.x 的原始碼:
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
如上所述,我們可以看到 Vue.js 針對陣列的 prototype
進行改寫,根據 JavaScript 的原型鏈機制,找到方法或屬性後就不會再往上找。
在 arrayProto[method]
取得原始方法,並且由 def
重新定義在 arrayProto[method]
的那幾種方法。
所以,當開發者呼叫上述提到的陣列中這七種方法,其實是調用被 Vue.js 攔截包裝後的方法, 既增加了自訂的邏輯,同時也調用陣列原始的方法,所以不會對原本功能產生影響。
另外,除了上述七種包裝過的陣列方法外,也可以透過 vm.$set(vm.items, indexOfItem, newValue)
來強制更新內容,
像是前面的 changeVal
就可以改寫成:
methods: {
changeVal () {
this.$set(this.items, 0, this.text)
alert(this.items);
}
}
2
3
4
5
6
於是,透過上面幾種方法改變狀態後的陣列,也會即時反映到畫面上了。
小提醒
自 Vue.js 3.0 之後,底層狀態的更新機制將由原本的 Object.defineProperty
改為 Proxy
API 來處理,此處提到的陣列更新就不會有無法偵測的問題了。
有關 Vue 3.0 後的更新機制,我們將會在後續介紹 3.0 新增的 Composition 一節中再次深入探討。