本文 AI 產出,尚未審核

Vue3 課程 – 響應式系統(Reactivity System)

主題:watch vs watchEffect 差別


簡介

在 Vue 3 中,響應式系統是框架的核心,也是開發者能夠以宣告式方式描述 UI 與資料關係的關鍵。當資料變動時,Vue 會自動觸發相應的更新,讓我們不必手動操作 DOM。

在實作上,Vue 提供了兩種「監聽」機制:watchwatchEffect。雖然它們看起來功能相近,但 設計目的、使用時機與行為細節都有所不同。了解兩者的差別,能讓你在開發過程中選擇最合適的工具,避免不必要的效能浪費或錯誤行為。

本文將從概念、語法、實作細節出發,透過多個實用範例說明 watchwatchEffect 的差異,並提供常見陷阱、最佳實踐與實務應用場景,幫助你在 Vue 3 中更得心應手地使用響應式監聽。


核心概念

1. watch:精準監聽特定來源

watch 允許你明確指定要監聽的響應式來源(單一值、計算屬性或是 getter 函式),並在來源變化時執行回呼函式。它的特點包括:

特性 說明
來源明確 必須傳入要觀察的 ref、reactive 屬性或 getter 函式。
延遲執行 只有當來源真正變化時才觸發,且預設在「微任務」結束後執行(可透過 flush: 'post' 改變時機)。
可取得舊值/新值 回呼函式的參數為 (newValue, oldValue, onCleanup),方便比對差異或執行清理工作。
支援深度監聽 透過 deep: true 可以遞迴觀察物件內層屬性變化。
可返回清理函式 onCleanup 允許在下一次執行前先釋放資源(如取消訂閱、關閉連線)。

範例 1:監聽單一 ref

import { ref, watch } from 'vue'

const count = ref(0)

// 只在 count 改變時觸發
watch(count, (newVal, oldVal) => {
  console.log(`count 從 ${oldVal} 變成 ${newVal}`)
})

// 測試變更
count.value++   // console: count 從 0 變成 1

範例 2:監聽多個來源(陣列寫法)

import { ref, watch } from 'vue'

const firstName = ref('John')
const lastName  = ref('Doe')

watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`姓名從 ${oldFirst} ${oldLast} 改成 ${newFirst} ${newLast}`)
})

// 改變任一欄位皆會觸發
lastName.value = 'Smith'   // console: 姓名從 John Doe 改成 John Smith

範例 3:深度監聽 reactive 物件

import { reactive, watch } from 'vue'

const profile = reactive({
  name: 'Alice',
  address: {
    city: 'Taipei',
    zip: '100'
  }
})

// deep: true 讓內層屬性變化也能被偵測
watch(
  () => profile,
  (newProfile, oldProfile) => {
    console.log('profile 變更', newProfile)
  },
  { deep: true }
)

// 變更內層屬性即觸發
profile.address.city = 'New Taipei'   // console: profile 變更 {...}

2. watchEffect:自動收集依賴、立即執行

watchEffect 採用 「副作用函式」(effect function)的概念:在函式內部直接使用響應式資料,Vue 會自動追蹤所有被讀取的依賴,並在任一依賴變化時重新執行整個函式。其特點:

特性 說明
自動依賴收集 不需要顯式指定來源,只要在函式裡使用了響應式值,就會被追蹤。
立即執行 副作用函式在建立時會立即執行一次(相當於 immediate: true)。
無新舊值參數 回呼僅接受 onCleanup,無法直接取得變更前後的值。
適合簡單副作用 如同步更新 DOM、觸發 API 請求、設定計時器等。
可設定執行時機 flush 選項可控制在「同步」('sync')、'pre'(預渲染前)或 'post'(渲染後)執行。

範例 4:最簡單的 watchEffect

import { ref, watchEffect } from 'vue'

const message = ref('Hello')
watchEffect(() => {
  console.log('訊息變更為:', message.value)
})

// 變更會自動觸發
message.value = 'World'   // console: 訊息變更為: World

範例 5:使用 onCleanup 取消計時器

import { ref, watchEffect } from 'vue'

const seconds = ref(0)

watchEffect((onCleanup) => {
  const timer = setInterval(() => {
    seconds.value++
  }, 1000)

  // 每次副作用重新執行前,先清除上一次的計時器
  onCleanup(() => clearInterval(timer))
})

範例 6:結合 flush: 'post' 與 UI 更新

import { ref, watchEffect } from 'vue'

const input = ref('')

// 先等 Vue 完成 DOM 更新後,再執行副作用(如聚焦)
watchEffect(
  (onCleanup) => {
    const el = document.getElementById('myInput')
    if (el) el.focus()
  },
  { flush: 'post' }
)

3. 何時選擇 watch、何時選擇 watchEffect

情境 建議使用 為什麼
需要比較新舊值 watch 可直接取得 newValoldVal
監聽多個來源且需要分別處理 watch 可以傳入陣列或 getter,且回呼會一次收到所有值。
只想要在資料變化時執行副作用,且不在意來源 watchEffect 自動收集依賴,寫法更簡潔。
需要深度監聽物件 watchdeep: true)或 watchEffect(自動深度追蹤) watchEffect 會追蹤所有使用到的屬性;watch 需要明確設定。
需要在副作用執行前先清理資源 兩者皆可,watch 使用 onCleanup 參數,watchEffect 直接接受 onCleanup
想要在組件掛載時立即執行一次 watchEffect(自動)或 watch(..., { immediate: true }) 兩者皆可,但 watchEffect 更直觀。
需要控制執行時機(pre / post) 兩者皆支援 flush 選項,但 watchEffect 在副作用函式內部更常使用。

常見陷阱與最佳實踐

1. 不小心造成無限迴圈

watchEffect(() => {
  // 若在副作用裡直接改變被追蹤的值,會立即觸發再次執行
  count.value++   // ❌ 無限遞增
})

解法:在副作用中避免直接改變同一個依賴,或使用 watch 並在回呼裡加入條件判斷。

2. deep: true 的效能成本

深度監聽會遍歷整個物件樹,對大型資料結構可能造成性能瓶頸。建議

  • 只在必要時使用 deep: true
  • 若只關注特定屬性,改用多個 watchwatchEffect 只讀取需要的欄位。

3. watchEffect 失去新舊值資訊

有時候需要比較變更前後的值,卻誤用 watchEffect,導致無法取得舊值。解法:改用 watch,或自行在 watchEffect 內部儲存前一次的值。

let prev = null
watchEffect(() => {
  const cur = data.value
  if (prev !== null) {
    console.log('變更前後:', prev, '→', cur)
  }
  prev = cur
})

4. 清理函式遺漏

在使用計時器、WebSocket、或第三方庫時,忘記在 onCleanup 中釋放資源會導致記憶體泄漏。最佳實踐

watchEffect((onCleanup) => {
  const socket = new WebSocket(url)
  socket.addEventListener('message', handler)

  onCleanup(() => {
    socket.removeEventListener('message', handler)
    socket.close()
  })
})

5. 依賴收集錯誤(使用非響應式值)

watchEffect 只會追蹤 響應式 讀取。如果在副作用裡使用普通變數或 props 的解構,依賴不會被收集,導致不會重新執行。

// ❌ 錯誤範例
const { title } = props   // 解構會失去響應性
watchEffect(() => {
  console.log(title)      // 改變 props.title 不會觸發
})

正確寫法

watchEffect(() => {
  console.log(props.title)   // 直接讀取 props.title
})

實際應用場景

1. 表單驗證(使用 watch

在複雜表單中,需要根據多個欄位的變化計算驗證結果,且必須比較前後值以決定是否顯示錯誤訊息。watch 的多來源與舊值參數最適合此情境。

import { reactive, watch } from 'vue'

const form = reactive({
  email: '',
  password: '',
  confirm: ''
})

watch(
  () => [form.email, form.password, form.confirm],
  ([email, password, confirm], [oldEmail]) => {
    // 只在任一欄位變更時重新驗證
    const errors = {}
    if (!/.+@.+\..+/.test(email)) errors.email = 'Email 格式不正確'
    if (password.length < 6) errors.password = '密碼長度不足'
    if (password !== confirm) errors.confirm = '密碼不一致'
    console.log('驗證結果', errors)
  },
  { immediate: true }
)

2. 動態資料抓取(使用 watchEffect

當某個搜尋關鍵字改變時,需要立即發送 API 請求並更新列表。watchEffect 可以自動追蹤關鍵字的變化,且結合 onCleanup 取消前一次的請求。

import { ref, watchEffect } from 'vue'
import axios from 'axios'

const keyword = ref('')
const results = ref([])
const controller = ref(null)   // 用於取消前一次請求

watchEffect((onCleanup) => {
  if (!keyword.value) {
    results.value = []
    return
  }

  // 取消上一次的請求
  if (controller.value) controller.value.abort()
  controller.value = new AbortController()

  axios
    .get('/api/search', {
      params: { q: keyword.value },
      signal: controller.value.signal
    })
    .then(res => (results.value = res.data))
    .catch(err => {
      if (err.name !== 'AbortError') console.error(err)
    })

  // 清理:在下一次關鍵字變更前取消請求
  onCleanup(() => {
    if (controller.value) controller.value.abort()
  })
})

3. UI 同步動畫(使用 watchEffect + flush: 'post'

在 Vue 組件掛載後,需要根據計算屬性產生的尺寸來設定 CSS 動畫。使用 watchEffect 並設定 flush: 'post',保證在 DOM 完全渲染後才執行動畫。

import { ref, computed, watchEffect } from 'vue'

const items = ref([...])   // 陣列資料
const containerHeight = computed(() => items.value.length * 40 + 'px')

watchEffect(
  (onCleanup) => {
    const el = document.querySelector('.list')
    el.style.height = containerHeight.value
    // 觸發 CSS transition
    requestAnimationFrame(() => {
      el.classList.add('expand')
    })
  },
  { flush: 'post' }
)

總結

  • watch「明確監聽」 工具,適合需要 新舊值比較、深度監聽或多來源監聽 的情境。它提供 immediatedeepflush 等選項,讓開發者可以精細控制執行時機與行為。
  • watchEffect 則是 「自動依賴收集」 的簡潔寫法,適合 單純副作用(如 UI 更新、API 請求、計時器)且不需要手動指定來源。它會在建立時立即執行,並在任何被使用的響應式值變化時重新執行。
  • 兩者都支援 清理函式 (onCleanup) 以防止資源泄漏;但要注意 避免在副作用內直接改變同一個依賴,以免產生無限迴圈。
  • 在實務開發中,先思考需求:是否需要比較前後值、是否要深度監聽、或只是單純的副作用?根據需求選擇最合適的 API,才能寫出 可讀、效能佳、易維護 的程式碼。

掌握 watchwatchEffect 的差異與最佳實踐,將大幅提升你在 Vue 3 中開發響應式應用的效率與品質。祝開發順利,期待你在專案中玩出更多創意!