v-for — 列表渲染
v-for 把陣列或物件的每一項渲染成對應的 DOM 元素,是列表渲染的基本款。
<script setup>
import { ref } from 'vue'
const fruits = ref(['蘋果', '香蕉', '橘子'])
</script>
<template>
<ul>
<li v-for="fruit in fruits" :key="fruit">
{{ fruit }}
</li>
</ul>
</template>
key 的作用
- Vue 的 diff 演算法靠
key認出每個元素是誰,才能決定哪些 DOM 節點該新增、刪除、移動 - 沒給
key的話,Vue 會「就地複用」——直接重用現有的 DOM 元素,只換掉內容 - 列表項目帶有內部狀態時(例如 input 的輸入值),就地複用會讓狀態跟著錯位
反面教材:用 index 當 key
<!-- ❌ 不建議 -->
<li v-for="(item, index) in list" :key="index">
假設列表是 [A, B, C],刪掉 B 之後剩 [A, C],index key 變成 [0, 1]。Vue 會誤以為 key=1 的內容從 B「變成」了 C,多做一次沒必要的更新,內部狀態也跟著錯位。
正確做法:用唯一 ID
<!-- ✅ 推薦 -->
<li v-for="item in list" :key="item.id">
v-for 搭配物件
<div v-for="(value, key, index) in userInfo" :key="key">
{{ index }}. {{ key }}: {{ value }}
</div>
v-for 的範圍
<!-- 渲染 1 到 10 -->
<span v-for="n in 10" :key="n">{{ n }}</span>
v-if vs v-show — 條件渲染
v-if
<div v-if="isLoggedIn">
歡迎回來!
</div>
<div v-else-if="isLoading">
載入中...
</div>
<div v-else>
請先登入
</div>
v-if 是真正的條件渲染:條件是 false 時,DOM 元素根本不會建立;變 true 才建立,再變回 false 就直接銷毀。
v-show
<div v-show="isVisible">
我是一段文字
</div>
v-show 只是切換 CSS 的 display: none,DOM 元素一直都在。
比較
| v-if | v-show | |
|---|---|---|
| 原理 | 建立/銷毀 DOM | 切換 display: none |
| 初始成本 | 低(false 時不渲染) | 高(不管怎樣都會渲染) |
| 切換成本 | 高(每次都要建立/銷毀) | 低(只是改 CSS) |
| 適合 | 條件很少改變 | 頻繁切換 |
- 頻繁切換的東西(tab、tooltip)用
v-show - 條件很少變的(登入/未登入)用
v-if
v-if 和 v-for 不要放在同一個元素上
<!-- ❌ 不要這樣 -->
<li v-for="item in list" v-if="item.isActive" :key="item.id">
<!-- ✅ 用 computed 先過濾,或包一層 template -->
<template v-for="item in list" :key="item.id">
<li v-if="item.isActive">{{ item.name }}</li>
</template>
在 Vue 3 裡 v-if 的優先權比 v-for 高,所以 v-if 那邊根本拿不到 item。
更好的做法是用 computed 先把列表過濾好:
const activeItems = computed(() => list.value.filter(item => item.isActive))
再用 v-for="item in activeItems" 渲染就行。
生命週期 — Lifecycle Hooks
一個 Vue 元件會經歷建立、掛載、更新、銷毀幾個階段,每個階段都有對應的 hook,讓你在那個時間點插入自己的邏輯。
Composition API 常用 hooks
import { onMounted, onUnmounted, onUpdated } from 'vue'
onMounted(() => {
// 元件掛載到 DOM 之後
// 適合:取得 DOM 元素、打初始 API、啟動計時器
console.log('我出生了!')
})
onUpdated(() => {
// 響應式資料變了,DOM 更新之後
// 適合:DOM 更新後要做的事(但通常用不太到)
console.log('我變了!')
})
onUnmounted(() => {
// 元件從 DOM 移除之後
// 適合:清除計時器、取消訂閱、移除事件監聽
console.log('我走了,再見!')
})
完整的生命週期
| Hook | 觸發時機 | 常見用途 |
|---|---|---|
onBeforeMount | 掛載之前 | 幾乎不用 |
onMounted | 掛載完成 | 打 API、操作 DOM |
onBeforeUpdate | DOM 更新前 | 幾乎不用 |
onUpdated | DOM 更新後 | 讀取更新後的 DOM |
onBeforeUnmount | 卸載前 | 清理工作(計時器等) |
onUnmounted | 卸載完成 | 最終清理 |
實務上最常用的就是 onMounted 和 onUnmounted 這一對。
範例:打 API
<script setup>
import { ref, onMounted } from 'vue'
const users = ref([])
const isLoading = ref(true)
onMounted(async () => {
try {
const res = await fetch('/api/users')
users.value = await res.json()
} finally {
isLoading.value = false
}
})
</script>
範例:清理計時器
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const now = ref(new Date())
let timer
onMounted(() => {
timer = setInterval(() => {
now.value = new Date()
}, 1000)
})
onUnmounted(() => {
clearInterval(timer) // 不清的話會 memory leak!
})
</script>
Composable — 組合式函數
Composable 就是把可複用的響應式邏輯抽成一個獨立函式,讓多個元件共用。
問題:重複邏輯
假設好幾個元件都要追蹤滑鼠位置。沒有 composable 的話,同一段邏輯就得在每個元件各寫一遍:
// 元件 A
const x = ref(0)
const y = ref(0)
function update(e) { x.value = e.pageX; y.value = e.pageY }
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 元件 B — 又寫一遍
// 元件 C — 再寫一遍
用 Composable 抽出來
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
元件中使用:
<script setup>
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>
<template>
<p>滑鼠位置:{{ x }}, {{ y }}</p>
</template>
一行搞定!而且邏輯只寫一次,改一個地方就全部更新。
命名慣例
Composable 慣例用 use 開頭命名:useMouse、useFetch、useAuth、useLocalStorage… 這不是硬性規定,是社群約定俗成——看到 use 開頭就知道是 composable。
實戰範例:useFetch
封裝一個通用的資料請求 composable:
// composables/useFetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const isLoading = ref(true)
fetch(url)
.then(res => res.json())
.then(json => { data.value = json })
.catch(err => { error.value = err })
.finally(() => { isLoading.value = false })
return { data, error, isLoading }
}
使用:
<script setup>
import { useFetch } from '@/composables/useFetch'
const { data: users, error, isLoading } = useFetch('/api/users')
</script>
<template>
<div v-if="isLoading">載入中...</div>
<div v-else-if="error">出錯了:{{ error.message }}</div>
<ul v-else>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
</template>
實戰範例:useLocalStorage
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const stored = localStorage.getItem(key)
const data = ref(stored ? JSON.parse(stored) : defaultValue)
watch(data, (newVal) => {
localStorage.setItem(key, JSON.stringify(newVal))
}, { deep: true })
return data
}
使用:
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'
const theme = useLocalStorage('theme', 'dark')
// 改 theme.value 會自動存到 localStorage
</script>
Composable vs Mixin
如果你接觸過 Vue 2,可能用過 Mixin。Composable 基本上是 Mixin 的上位替代:
| Mixin | Composable | |
|---|---|---|
| 命名衝突 | 會(屬性名重複就互相覆蓋) | 不會(你自己解構命名) |
| 資料來源 | 不清楚(來自哪個 mixin?) | 清楚(import 就知道) |
| TypeScript 支援 | 差 | 好 |
所以 Vue 3 基本上不推薦用 Mixin 了,composable 才是正解。
小結
| 工具 | 一句話 |
|---|---|
v-for | 列表渲染,記得加唯一 :key |
v-if / v-show | 條件渲染,少切換用 if、頻繁切換用 show |
| 生命週期 | 記 onMounted 和 onUnmounted 就夠了 |
| Composable | 把共用邏輯抽成 use 開頭的函式 |
這四個東西是 Vue 開發的日常。掌握了它們,加上前面文章講的響應式和資料流,你就已經有能力寫出一個完整的 Vue 應用了。接下來我們會進入路由(Vue Router)和狀態管理(Pinia),那就是大型應用的領域了。
Latest Updates
- 2026.06.11 Content updated
