什麼是「響應式」?
- 核心概念:資料變了,畫面自動跟著變
- 不用自己動手操作 DOM(
document.querySelector/innerHTML),這些 Vue 都會處理好
底層原理
Vue 3 的響應式是用 JavaScript 的 Proxy 做的:把資料包成 Proxy 物件,讀取和修改都會被攔截到,Vue 就趁這個時機追蹤依賴、觸發畫面更新。
// 概念上大概長這樣(不是真正的原始碼)
const data = new Proxy({ count: 0 }, {
get(target, key) {
// 記錄:誰在讀這個資料
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
// 通知:這個資料被改了,去更新畫面
trigger(target, key)
return true
}
})
- Vue 2 用的是
Object.defineProperty,偵測不到「新增屬性」和「刪除屬性」,所以才需要Vue.set()這種 API - Vue 3 改用
Proxy之後,這個限制就不存在了
ref — 最常用的響應式 API
ref 是 Vue 3 Composition API 裡最常用的響應式 API。
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
重點
- 在 JS 裡要加
.value—ref回傳的是一個包裝物件,值放在.value裡 - 在 template 裡不用加 — Vue 會自動解包(unwrap),直接寫
{{ count }}就好 - 什麼型別都能包 — 字串、數字、布林、陣列、物件都行
const name = ref('Jeremy') // 字串
const isLoading = ref(false) // 布林
const items = ref([1, 2, 3]) // 陣列
const user = ref({ name: 'Aira' }) // 物件
為什麼需要 .value?
JavaScript 的基本型別(string、number、boolean)是傳值的,Proxy 追蹤不到。所以 ref 把值包進一個物件,存取都走 .value,Proxy 才有東西可以攔。Template 裡 Vue 幫你自動解包,所以不用寫 .value。
reactive — 物件的響應式
reactive 是物件專用的響應式宣告,好處是存取屬性不用 .value。
<script setup>
import { reactive } from 'vue'
const state = reactive({
count: 0,
name: 'Jeremy'
})
function increment() {
state.count++ // 不用 .value!
}
</script>
<template>
<p>{{ state.count }} - {{ state.name }}</p>
</template>
ref vs reactive
Vue 官方推薦優先使用
ref。
因為 reactive 有幾個坑:
限制 1:不能整個替換
let state = reactive({ count: 0 })
// ❌ 這樣會失去響應式!
state = reactive({ count: 1 })
// 變數指向新物件,舊 Proxy 連結中斷
限制 2:解構會失去響應式
const state = reactive({ count: 0, name: 'Jeremy' })
// ❌ 解構出來的值不是響應式的!
const { count } = state
// count 只是普通數字 0,不會觸發更新
限制 3:不能用在基本型別
// ❌ 這不行,reactive 只接受物件
const count = reactive(0)
這些坑 ref 都沒有,所以大部分情境直接用 ref 就好。
適合使用 reactive 的場景
一組關係緊密的狀態,而且確定不會整個替換掉——這種時候 reactive 用起來比較順手:
// 表單資料很適合用 reactive
const form = reactive({
username: '',
email: '',
password: ''
})
不過用 ref 包物件也能做到一樣的事:
const form = ref({
username: '',
email: '',
password: ''
})
// 只是要多一個 .value
form.value.username = 'jeremy'
computed — 計算屬性
computed 用來從其他響應式資料算出一個新值,而且自帶快取。
<script setup>
import { ref, computed } from 'vue'
const price = ref(100)
const quantity = ref(3)
// 總價 = 單價 × 數量,自動算
const total = computed(() => price.value * quantity.value)
</script>
<template>
<p>總價:{{ total }}</p>
</template>
computed vs method
// computed
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// method
function getFullName() {
return `${firstName.value} ${lastName.value}`
}
用起來好像差不多?差別在於快取。
computed有快取:只有依賴的資料變了才會重新計算。如果firstName和lastName都沒變,不管你在 template 裡用幾次{{ fullName }},都只算一次。method沒快取:每次 template 重新渲染,函式就被執行一次。
用生活比喻:computed 就像冰箱裡的便當,做好了就放著,要吃的時候直接拿。method 就像現點現做的餐廳,每次都要重新煮。
所以如果計算成本比較高(比如要 filter 一個很大的陣列),用 computed 效能會好很多。
computed 也可以寫入
預設的 computed 是唯讀的,但你也可以給它 getter 和 setter:
const firstName = ref('Jeremy')
const lastName = ref('Ho')
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val) => {
const [first, last] = val.split(' ')
firstName.value = first
lastName.value = last
}
})
fullName.value = 'Aira Chen' // 會觸發 setter
不過實務上可寫的 computed 比較少見,大部分時候用唯讀就夠了。
watch — 監聽資料變化
watch 讓你在資料變化的時候「做一些事」。跟 computed 不同的是,watch 不是要算出新值,而是要執行副作用(side effect),比如打 API、更新 localStorage、console.log 之類的。
import { ref, watch } from 'vue'
const keyword = ref('')
// 當 keyword 改變時,打搜尋 API
watch(keyword, (newVal, oldVal) => {
console.log(`從 "${oldVal}" 變成 "${newVal}"`)
fetchSearchResults(newVal)
})
監聽多個來源
const firstName = ref('Jeremy')
const lastName = ref('Ho')
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`姓名從 ${oldFirst} ${oldLast} 變成 ${newFirst} ${newLast}`)
})
監聽 reactive 物件
const state = reactive({ count: 0 })
// 監聽 reactive 物件的某個屬性,要用 getter 函式
watch(
() => state.count,
(newCount) => {
console.log('count 變了:', newCount)
}
)
深層監聽
如果你監聽一個 ref 包的物件,預設只會在「整個物件被替換」時觸發。要監聽物件裡面的屬性變化,得加 deep: true:
const user = ref({ name: 'Jeremy', age: 28 })
watch(user, (newUser) => {
console.log('user 變了')
}, { deep: true })
注意:深層監聽效能比較差,因為 Vue 要遞迴遍歷整個物件。如果你只是要監聽某個特定屬性,用 getter 函式比較好。
watchEffect — watch 的懶人版
watchEffect 跟 watch 很像,但它不需要你指定要監聽什麼。它會自動收集依賴 — 你在 callback 裡面用了什麼響應式資料,它就監聽什麼。
import { ref, watchEffect } from 'vue'
const count = ref(0)
const name = ref('Jeremy')
watchEffect(() => {
// Vue 會自動偵測到這裡用了 count 和 name
console.log(`${name.value} 的計數:${count.value}`)
})
watch vs watchEffect
| watch | watchEffect | |
|---|---|---|
| 需要指定監聽來源 | 是 | 不需要,自動收集 |
| 可以拿到舊值 | 可以 (oldVal) | 不行 |
| 初次執行 | 預設不會(除非加 immediate: true) | 會立即執行一次 |
| 適合場景 | 需要比較新舊值、條件觸發 | 簡單的副作用,不在意舊值 |
用生活比喻:
watch像保全監視器 — 你告訴它「盯著大門」,有人進出它就通報。你可以知道「誰進來了」和「誰出去了」。watchEffect像一隻很敏感的狗 — 你不用告訴它盯什麼,只要附近有動靜(你在 callback 裡用到的任何響應式資料變了),它就會叫。但它不會告訴你「之前是什麼狀況」。
停止監聽
不管是 watch 還是 watchEffect,都會回傳一個「停止函式」。元件卸載時它們會自動停止,但如果你想提前停掉:
const stop = watchEffect(() => {
console.log(count.value)
})
// 不想監聽了
stop()
小結
| API | 用途 | 一句話 |
|---|---|---|
ref | 宣告響應式資料 | 萬用,什麼都能包 |
reactive | 宣告響應式物件 | 方便但有坑,新手先用 ref |
computed | 根據其他資料算出新值 | 有快取的公式 |
watch | 資料變了做副作用 | 精準監控,可拿新舊值 |
watchEffect | 資料變了做副作用 | 自動收集,懶人用 |
記住一個原則:大部分時候用 ref + computed 就能搞定 80% 的需求。watch 留給需要執行副作用的場景(打 API、操作 DOM 之類的),reactive 留給你真的很確定要用的時候。
其實 Vue 3 的響應式系統設計得相當優雅,一旦理解了「資料變 → 畫面自動更新」這個核心概念,剩下的就只是選擇用哪個 API 比較適合而已。
Latest Updates
- 2026.06.11 Content updated
