vue reactivity ref reactive computed watch

[Vue] 響應式系統

什麼是「響應式」?

  • 核心概念:資料變了,畫面自動跟著變
  • 不用自己動手操作 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 裡要加 .valueref 回傳的是一個包裝物件,值放在 .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 有快取:只有依賴的資料變了才會重新計算。如果 firstNamelastName 都沒變,不管你在 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 的懶人版

watchEffectwatch 很像,但它不需要你指定要監聽什麼。它會自動收集依賴 — 你在 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

watchwatchEffect
需要指定監聽來源不需要,自動收集
可以拿到舊值可以 (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