vue v-for v-if lifecycle composable

[Vue] 常用工具

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-ifv-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
onBeforeUpdateDOM 更新前幾乎不用
onUpdatedDOM 更新後讀取更新後的 DOM
onBeforeUnmount卸載前清理工作(計時器等)
onUnmounted卸載完成最終清理

實務上最常用的就是 onMountedonUnmounted 這一對。

範例:打 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 開頭命名:useMouseuseFetchuseAuthuseLocalStorage… 這不是硬性規定,是社群約定俗成——看到 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 的上位替代:

MixinComposable
命名衝突會(屬性名重複就互相覆蓋)不會(你自己解構命名)
資料來源不清楚(來自哪個 mixin?)清楚(import 就知道)
TypeScript 支援

所以 Vue 3 基本上不推薦用 Mixin 了,composable 才是正解。


小結

工具一句話
v-for列表渲染,記得加唯一 :key
v-if / v-show條件渲染,少切換用 if、頻繁切換用 show
生命週期onMountedonUnmounted 就夠了
Composable把共用邏輯抽成 use 開頭的函式

這四個東西是 Vue 開發的日常。掌握了它們,加上前面文章講的響應式和資料流,你就已經有能力寫出一個完整的 Vue 應用了。接下來我們會進入路由(Vue Router)和狀態管理(Pinia),那就是大型應用的領域了。

Latest Updates

  • 2026.06.11 Content updated