本文 AI 產出,尚未審核

Vue3 Composition API:watch()watchEffect() 完全解析


簡介

在 Vue3 中,Composition API 讓我們以函式的方式組織邏輯,reactiverefcomputed 等 API 已經相當熟悉,接下來最常用的兩個監控工具——watch()watchEffect(),則是處理副作用 (side‑effects) 的關鍵。

  • watch()精準監聽特定的響應式來源(單一變數、陣列或物件),適合需要在值變化前後取得舊值與新值的情境。
  • watchEffect()自動追蹤在回呼函式內使用的所有響應式來源,類似 computed 的依賴收集,但不會回傳值,而是直接執行副作用。

掌握這兩個 API,不僅能讓資料變化時即時同步 UI、發送請求或清理資源,更能避免不必要的重新渲染與效能浪費。以下將從概念、語法、實作範例一路說到常見陷阱與最佳實踐,讓你在專案中得心應手。


核心概念

1. watch() 的基本語法

import { ref, watch } from 'vue'

const count = ref(0)

// 監聽單一 ref,回呼 receives (newVal, oldVal)
watch(count, (newVal, oldVal) => {
  console.log(`count 從 ${oldVal} 變成 ${newVal}`)
})
  • 第一個參數:要監聽的來源,可以是 refreactive、getter 函式,或是包含多個來源的陣列。
  • 第二個參數:副作用回呼,預設接受 (newValue, oldValue)
  • 第三個參數(選填):{ immediate, deep, flush } 讓你控制首次執行、深層偵測與執行時機。

watch 只會在 變更後(micro‑task)觸發,除非設定 flush: 'sync'

2. watchEffect() 的簡潔寫法

import { ref, watchEffect } from 'vue'

const name = ref('Alice')
const greeting = ref('')

// 自動追蹤 name,name 變動時重新執行
watchEffect(() => {
  greeting.value = `Hello, ${name.value}!`
  console.log(greeting.value)
})
  • 不需要明確宣告要監聽的來源,Vue 會在執行函式時「捕捉」所有被存取的響應式屬性。
  • 回呼沒有參數,因此只能取得最新值,若需要舊值請改用 watch()
  • 預設 同步執行flush: 'pre'),適合更新 UI 前的副作用。

3. deep 監聽:觀測物件內部變化

import { reactive, watch } from 'vue'

const user = reactive({
  profile: {
    name: 'Bob',
    age: 30
  }
})

// 深層偵測 profile 內任意屬性變化
watch(
  () => user.profile,
  (newVal, oldVal) => {
    console.log('profile 變更:', newVal)
  },
  { deep: true }
)
  • 若不加 deep:true,只會偵測 引用變更(即 user.profile = {...}),不會捕捉內部屬性的修改。

4. immediate:首次執行監聽

watch(
  count,
  (newVal) => {
    console.log('首次或之後的 count:', newVal)
  },
  { immediate: true }   // 立即執行一次
)
  • 常用於 初始化 時需要根據當前值執行一次副作用(例如載入遠端資料)。

5. 清理函式(onInvalidate

watch()watchEffect() 都支援在每次重新執行前清理舊的副作用,常見於 防抖、取消請求 等情境。

import { ref, watchEffect } from 'vue'

const query = ref('vue')
let controller = null

watchEffect((onInvalidate) => {
  // 取消上一次的請求
  if (controller) controller.abort()
  controller = new AbortController()

  fetch(`https://api.example.com/search?q=${query.value}`, {
    signal: controller.signal
  })
    .then(res => res.json())
    .then(data => console.log(data))

  // 設定清理函式
  onInvalidate(() => {
    controller.abort()
    console.log('上一個請求已取消')
  })
})

程式碼範例(實用案例)

範例 1️⃣:表單驗證的即時回饋(watchEffect

import { ref, watchEffect } from 'vue'

export default {
  setup() {
    const email = ref('')
    const emailError = ref('')

    // 每次 email 改變即時驗證
    watchEffect(() => {
      const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
      emailError.value = pattern.test(email.value) ? '' : '請輸入有效的 Email'
    })

    return { email, emailError }
  }
}

重點watchEffect 只要 email 被讀取,就會自動重新執行驗證邏輯,寫法比 watch(email, ...) 更簡潔。

範例 2️⃣:路由參數變化時重新抓取資料(watch + immediate

import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'

export default {
  setup() {
    const route = useRoute()
    const post = ref(null)

    const fetchPost = async (id) => {
      const res = await fetch(`https://api.example.com/posts/${id}`)
      post.value = await res.json()
    }

    // 監聽路由參數 id,首次立即執行
    watch(
      () => route.params.id,
      (newId) => {
        fetchPost(newId)
      },
      { immediate: true }
    )

    return { post }
  }
}

範例 3️⃣:深層偵測設定物件的變更(watch + deep

import { reactive, watch } from 'vue'

export default {
  setup() {
    const settings = reactive({
      theme: { dark: false },
      layout: { sidebar: true }
    })

    watch(
      () => settings,
      (newVal) => {
        console.log('設定變更,寫入 localStorage')
        localStorage.setItem('app-settings', JSON.stringify(newVal))
      },
      { deep: true }
    )

    return { settings }
  }
}

範例 4️⃣:防抖搜尋(watch + onInvalidate

import { ref, watch } from 'vue'

export default {
  setup() {
    const keyword = ref('')
    const results = ref([])

    watch(
      keyword,
      (newVal, _, onInvalidate) => {
        const timer = setTimeout(async () => {
          const res = await fetch(`https://api.example.com/search?q=${newVal}`)
          results.value = await res.json()
        }, 300) // 300ms 防抖

        // 若 keyword 在 300ms 內再次改變,清除計時器
        onInvalidate(() => clearTimeout(timer))
      }
    )

    return { keyword, results }
  }
}

範例 5️⃣:自動取消未完成的 HTTP 請求(watchEffect + onInvalidate

import { ref, watchEffect } from 'vue'

export default {
  setup() {
    const userId = ref(1)
    const user = ref(null)

    watchEffect((onInvalidate) => {
      const controller = new AbortController()
      fetch(`https://api.example.com/users/${userId.value}`, {
        signal: controller.signal
      })
        .then(r => r.json())
        .then(data => (user.value = data))
        .catch(err => {
          if (err.name !== 'AbortError') console.error(err)
        })

      // 清理:切換 userId 時取消前一次請求
      onInvalidate(() => controller.abort())
    })

    return { userId, user }
  }
}

常見陷阱與最佳實踐

陷阱 說明 解決方案 / 實踐
過度使用 watchEffect watchEffect 會追蹤所有在回呼裡讀取的響應式值,若不慎把大量無關的邏輯放進去,會導致 不必要的重新執行 只在純副作用(如 DOM 操作、API 呼叫)使用 watchEffect,對於需要舊值或精確控制的情況改用 watch
忘記 deep: true 監聽 reactive 物件的屬性時,若只監聽引用,內部屬性變更不會觸發。 需要觀測深層結構時,加上 { deep: true },或改寫為 分段監聽(如 watch(() => obj.prop, ...))。
未使用 immediate 初始化時需要根據當前值執行一次副作用,卻忘記加 immediate,造成 UI 與資料不同步。 在需要「首次同步」的場景(如載入資料、設定預設值)加上 { immediate: true }
清理函式遺漏 異步請求、定時器或訂閱未在重新執行前取消,會產生 記憶體洩漏競爭條件 使用 onInvalidatewatch)或 onInvalidate 參數(watchEffect必寫清理邏輯。
flush 時機誤用 預設 flush: 'pre'(在 DOM 更新前),有時需要等 DOM 完成才執行(如操作實際元素),此時使用 flush: 'post' 根據需求選擇 prepostsync,尤其在操作外部 UI 套件時使用 post

最佳實踐小結

  1. 先思考是否需要舊值:若需要,使用 watch;若僅需最新值,watchEffect 更簡潔。
  2. 盡量限制監聽範圍:使用 getter 函式或陣列,只監聽必要的屬性,避免不必要的重算。
  3. 配合 onInvalidate:所有涉及非同步或資源占用的副作用,都應提供清理機制。
  4. 適時使用 flush:對於需要在 DOM 更新前/後執行的副作用,明確指定 flush,提升可預測性。
  5. 測試與除錯:在開發環境開啟 Vue Devtools,觀察 watch/watchEffect 的觸發次數,確保不會過度重複。

實際應用場景

場景 建議使用 為什麼
即時表單驗證 watchEffect 只要表單欄位被讀取即自動重新驗證,寫法最簡潔。
路由變更時重新抓取資料 watch + immediate 需要監聽特定路由參數變化,並在首次載入時立即執行一次。
全局設定(主題、語系)同步到 localStorage watch + deep 設定是深層物件,需捕捉任意屬性變更,並持久化。
搜尋框的防抖請求 watch + onInvalidate 必須在每次輸入變化時取消前一次的計時器,避免過多請求。
多頁面切換時取消未完成的 API watchEffect + onInvalidate 每次依賴變更(如 userId)都會自動取消舊的請求,避免競爭。
動畫庫或第三方 UI 元件的同步 watch + flush: 'post' 需要等 Vue 完成 DOM 更新後才調用外部庫的 API。

總結

  • watch()watchEffect() 是 Vue3 Composition API 中處理 副作用 的兩把利器。
  • watch() 精準、支援舊值、深層偵測與首次立即執行,適合需要細部控制的場景。
  • watchEffect() 自動追蹤所有在回呼裡使用的響應式來源,語法最簡潔,適合即時回饋一次性副作用
  • 正確使用 deepimmediateflush 以及 清理函式 (onInvalidate) 能避免效能問題與記憶體洩漏。
  • 在實務開發中,根據「是否需要舊值」與「監聽範圍」選擇合適的 API,搭配最佳實踐,就能寫出 可維護、效能佳 的 Vue3 應用。

掌握這兩個 API,你的 Vue3 專案將不再因資料變化而手忙腳亂,真正做到「資料驅動」的開發哲學。