iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Modern Web

重新認識Vue 走過路過不要錯過系列 第 16 篇

Vue 走過路過不要錯過 Day16 - props:defineProps 與單向資料流的規矩

  • 分享至 

  • xImage
  •  

Day16 - props:defineProps 與單向資料流的規矩

前言:同一張商品卡片,為什麼不能寫死?

假設你在做一個商品列表頁,每個商品都要顯示名稱、價格。最直覺的做法是把卡片的 HTML 直接複製貼上,再一張張改文字。商品只有三個的時候還撐得住,等到變成三十個,改一個樣式就要改三十個地方。

Day 15 我們已經學會把畫面拆成組件。拆出來的卡片組件只負責「長什麼樣子」,至於「顯示哪一個商品」,應該交給使用它的人決定。這個「由外面把資料傳進來」的機制,就是 props。

為什麼需要 props

如果一份資料不是全域都需要,只有某一塊畫面用得到,最自然的做法是由父組件握有這份資料,需要的時候再傳給子組件。子組件不用知道資料從哪來,只管拿到什麼就顯示什麼。這樣同一個組件餵不同的資料,就能長出不同的內容,復用起來也很輕鬆。

相對地,像是登入狀態、購物車這類很多頁面都要讀寫的資料,就不適合一層一層傳下去,那是全域狀態要處理的事(Pinia 之後會談到)。判斷的方式很簡單:只有這個組件和它的上下層用得到,就用 props;很多地方都要用,再考慮全域。

defineProps 的基本寫法

在 <script setup> 裡,用 defineProps 宣告這個組件接受哪些 props。它是編譯巨集,不需要 import 就能直接使用。

<script setup>
const props = defineProps({
  title: { type: String, required: true },
  price: { type: Number, default: 0 },
  tags: { type: Array, default: () => [] }
})
</script>

<template>
  <div class="card">
    <h3>{{ title }}</h3>
    <p>NT$ {{ price }}</p>
  </div>
</template>

幾個常見的重點:

  • 模板裡可以直接用 title,不用寫 props.title;在 script 裡則要透過 props.title 存取。
  • type 讓 Vue 在開發模式下幫你檢查型別,傳錯會在 console 出現警告。
  • required: true 代表一定要傳,default 是沒傳時的預設值。
  • 物件與陣列的預設值要寫成函式(() => []),避免多個組件實例共用同一個參考。

父組件這邊這樣使用:

<script setup>
import { ref } from 'vue'
import ProductCard from './ProductCard.vue'

const products = ref([
  { id: 1, title: '安全帽 A 款', price: 1200 },
  { id: 2, title: '安全帽 B 款', price: 1500 }
])
</script>

<template>
  <ProductCard
    v-for="item in products"
    :key="item.id"
    :title="item.title"
    :price="item.price"
  />
</template>

這裡有個新手常踩的小地方:如果寫成 price="1200"(沒有冒號),傳進去的是字串 "1200",會和 Number 型別對不上而出現警告。要傳「JS 的值」而不是純文字,記得加上 :(也就是 v-bind)。

單向資料流:資料只能由上往下

Vue 對 props 訂了一條規矩:資料由父組件流向子組件,子組件不應該反過來修改它。

先看最直接的違規寫法:

<script setup>
const props = defineProps({
  count: Number
})

function add() {
  props.count++ // 直接改 props
}
</script>

這樣寫,開發模式下 Vue 會在 console 警告 props 是唯讀的,值也不會被改掉。

但事情沒有這麼單純。如果傳進來的是物件呢?

<script setup>
const props = defineProps({
  product: { type: Object, required: true }
})

function toggleFavorite() {
  props.product.isFavorite = true // 改的是物件內部的屬性
}
</script>

這一次 Vue 不會警告,而且畫面還真的會更新。原因是 props 的唯讀只擋「整個 prop 被重新賦值」這一層,物件內部的屬性並沒有被凍結。子組件拿到的其實是父組件那個物件的同一個參考,所以這一行改到的是父組件的資料。

看起來能動,為什麼還是不好?

  • 資料來源變得不明確:想知道 isFavorite 是誰改的,你得翻遍所有拿到這個物件的子組件。
  • 一份資料被多處修改,除錯困難:如果同一個物件同時傳給好幾個子組件,任何一個都能偷偷改它,畫面出現異常時很難追查。
  • 組件失去獨立性:子組件悄悄依賴「父組件的資料可以被我改」,換個地方使用就可能出問題。

單向資料流的意義就在這裡:資料的修改權集中在擁有它的那一層,任何變動都能循著同一條路徑追蹤。

子組件真的需要「改」的時候怎麼辦

實務上常見的情況大致有三種,各有各的處理方式。

1. 只是把 props 當作初始值

例如傳進來一個初始數量,子組件之後自己管理。這時候複製一份到本地的 ref:

<script setup>
import { ref } from 'vue'

const props = defineProps({
  initialCount: { type: Number, default: 0 }
})

const count = ref(props.initialCount)
</script>

要注意這只會在建立時複製一次,之後父組件再改 initialCount,本地的 count 不會跟著變。

2. 需要根據 props 算出另一個值

例如傳進來原價,卡片要顯示打折後的價格。這種情況用 computed,會跟著 props 的變化自動更新:

<script setup>
import { computed } from 'vue'

const props = defineProps({
  price: { type: Number, default: 0 }
})

const salePrice = computed(() => Math.round(props.price * 0.8))
</script>

3. 子組件想「請父組件」修改資料

像前面的收藏按鈕,子組件不自己改資料,而是通知父組件「使用者按了收藏」,由父組件決定要不要改、怎麼改。這正是 emit 的工作,下一篇會詳細介紹。這邊先記住核心觀念:props 往下傳,事件往上報。

補充:解構 props 的注意事項

Day 5 我們談過,一般的 reactive 物件解構後會失去響應式。props 也有類似的情況:如果在 script 裡用 const { title } = props 這種寫法取值,之後 title 就只是一個普通變數,不會再跟著更新。

Vue 3.5 之後,直接在 defineProps 上解構(const { title } = defineProps(...))會由編譯器幫你保持響應式。但如果你的專案版本較舊,或是在其他地方解構,就要改用 toRefs(props) 或直接使用 props.title。

小結

  • props 讓父組件決定資料,子組件專心負責呈現,同一個組件可以輕鬆復用。
  • defineProps 是編譯巨集,不需要 import,可以搭配 type、required、default 做基本驗證。
  • 資料只能由上往下流。整個 prop 重新賦值會被 Vue 警告,但修改物件內部屬性不會,這是最容易忽略的陷阱。
  • 子組件需要「改」資料時:初始值用本地 ref、衍生值用 computed、要通知父組件則用 emit。

上一篇
Vue 走過路過不要錯過 Day15 - 組件拆分與生命週期:setup、onMounted、onUnmounted
下一篇
Vue 走過路過不要錯過 Day17 - emits:子組件怎麼把事情告訴父組件
系列文
重新認識Vue 走過路過不要錯過 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言