跳至主要內容
Skip to content

watch 與 computed 的區別

watchcomputed 都建立在 Vue 響應式系統上,也都會追蹤依賴。但它們解決的問題不同:computed 用來產生衍生值,watch 用來在資料變化時執行副作用。

一句話回答:如果你需要「根據現有狀態算出另一個值」,用 computed;如果你需要「資料變了之後做一件事」,例如請求 API、寫入 localStorage、操作 DOM,就用 watch


一、 必考觀念

1. computed 是衍生狀態

computed 適合描述「一個值如何由其他狀態推導而來」。

typescript
import { computed, ref } from 'vue'

const price = ref(100)
const quantity = ref(2)

const total = computed(() => {
  return price.value * quantity.value
})

total 不應該手動維護,因為它永遠可以由 pricequantity 算出來。

這種場景如果用 watch,反而會多一份同步成本:

typescript
const total = ref(0)

watch([price, quantity], () => {
  total.value = price.value * quantity.value
})

這樣 total 變成另一份可變狀態。如果同步邏輯漏掉或時機錯了,就會出現資料不一致。

2. watch 是副作用

watch 適合在資料變化後做某件事。

typescript
import { ref, watch } from 'vue'

const keyword = ref('')
const results = ref([])

watch(keyword, async (value) => {
  results.value = await searchUsers(value)
})

這裡的 API 請求就是副作用。它不是單純「算出一個值」,而是觸發外部行為。

常見副作用:

  • 請求 API。
  • 同步 localStorage。
  • 操作 DOM。
  • 呼叫第三方套件。
  • 根據 route params 載入資料。
  • 上報 analytics。

3. computed 有快取,watch 沒有「回傳值快取」概念

computed 的 getter 會被快取,依賴不變就不重新算。

typescript
const expensiveValue = computed(() => {
  return heavyCalculate(source.value)
})

多次讀取:

typescript
expensiveValue.value
expensiveValue.value
expensiveValue.value

只要 source.value 沒變,getter 不會重跑。

watch 不是用來產生可讀取的快取值。它關心的是「source 變了,callback 要不要執行」。

4. computed 通常應該是純函數

computed getter 應該只做同步計算並回傳值,不應該做副作用。

不建議:

typescript
const user = computed(async () => {
  return await fetchUser(id.value)
})

也不建議:

typescript
const total = computed(() => {
  localStorage.setItem('price', String(price.value))
  return price.value * quantity.value
})

這些應該交給 watch


二、 核心對比

面向computedwatch
核心用途產生衍生值執行副作用
是否回傳值是,回傳 computed ref否,回傳停止監聽函數
是否快取有,依賴不變不重算callback 依 source 變化執行
是否 lazy是,讀取時才求值預設 lazy,source 變化才 callback
是否適合 async不適合適合
是否應有副作用不應該可以
常見場景篩選列表、總價、格式化文字API、DOM、localStorage、第三方套件

三、 原理差異

1. computed 的心智模型

computed 是一個特殊 ref:

text
讀 computed.value
-> 如果 dirty,執行 getter
-> getter 期間收集依賴
-> 快取結果
-> 依賴沒變,下次直接回傳快取

依賴變化時:

text
dependency trigger
-> computed 標記 dirty
-> 通知依賴 computed 的 effect
-> 下次讀取 computed.value 時重新計算

如果你想看更細的實現,可以讀:Vue computed 的實現原理

2. watch 的心智模型

watch 是建立一個監聽 effect。它會追蹤 source 的變化,當新舊值不同時執行 callback。

typescript
watch(source, (newValue, oldValue) => {
  // source 變化後執行
})

簡化流程:

text
第一次收集 source 依賴
-> source 變化
-> scheduler 安排 callback
-> 重新取得 newValue
-> 比較 newValue / oldValue
-> 執行 callback

3. watch 預設也是 lazy

很多人以為 watch 會立刻執行。其實 watch 預設不會立即執行 callback,只有 source 變化後才執行。

typescript
watch(keyword, () => {
  console.log('changed')
})

如果你希望建立 watch 時立刻執行一次:

typescript
watch(
  keyword,
  () => {
    console.log('run now and when changed')
  },
  { immediate: true },
)

四、 watch 的常見選項

1. immediate

immediate 讓 callback 在建立 watch 時先執行一次。

typescript
watch(
  userId,
  async (id) => {
    user.value = await fetchUser(id)
  },
  { immediate: true },
)

適合「進頁面先載入一次,之後 id 變了再載入」。

2. deep

deep 用來深層監聽物件內部變化。

typescript
watch(
  form,
  () => {
    saveDraft(form)
  },
  { deep: true },
)

要小心:deep watch 需要遍歷深層資料,資料很大時可能有成本。很多時候更好的做法是 watch 精準欄位。

typescript
watch(
  () => form.email,
  (email) => {
    validateEmail(email)
  },
)

3. flush

flush 控制 callback 執行時機。

flush時機
pre預設,元件 DOM 更新前
post元件 DOM 更新後
sync同步執行

如果 callback 需要讀更新後的 DOM,可以用:

typescript
watch(
  visible,
  () => {
    inputRef.value?.focus()
  },
  { flush: 'post' },
)

這和 nextTick 很接近。延伸讀:nextTick 的作用與實現原理

4. cleanup

watch callback 可以註冊清理函數,避免競態或資源泄漏。

typescript
watch(keyword, async (value, _oldValue, onCleanup) => {
  const controller = new AbortController()

  onCleanup(() => {
    controller.abort()
  })

  results.value = await fetch(`/api/search?q=${value}`, {
    signal: controller.signal,
  }).then(res => res.json())
})

當 keyword 很快變化時,上一個請求可以被取消。


五、 watchEffect 是什麼?

watchEffect 會自動追蹤 callback 裡讀到的響應式依賴。

typescript
watchEffect(() => {
  console.log(userId.value)
  console.log(locale.value)
})

它不需要你明確指定 source。

對比:

typescript
watch([userId, locale], () => {
  // 明確監聽 userId 和 locale
})
API特點
watch明確 source,能拿 new/old value
watchEffect自動收集依賴,立即執行

watchEffect 適合依賴簡單、探索性或需要自動追蹤的副作用;watch 適合明確控制資料來源、比較新舊值、處理精準副作用。


六、 追問題庫

Q1:能用 watch 實現 computed 嗎?

可以,但不建議。

typescript
const fullName = ref('')

watch([firstName, lastName], () => {
  fullName.value = `${firstName.value} ${lastName.value}`
}, { immediate: true })

這會讓衍生值變成手動同步狀態。computed 更直覺,也更不容易出錯。

Q2:能用 computed 取代 watch 嗎?

如果只是算值,可以。

如果要做副作用,不應該。

typescript
// 適合 computed
const isValid = computed(() => email.value.includes('@'))

// 適合 watch
watch(isValid, (value) => {
  localStorage.setItem('valid', String(value))
})

Q3:computed 裡可以寫 async 嗎?

不建議。

computed 期望回傳同步衍生值。如果 getter 是 async,回傳的是 Promise,不是最終資料,而且快取的是 Promise 本身,容易讓狀態語意混亂。

Q4:watch 監聽物件時 newValue 和 oldValue 為什麼可能一樣?

如果是 deep watch 同一個 reactive object,內部屬性變化時,新舊值可能指向同一個物件引用。

typescript
watch(
  form,
  (newValue, oldValue) => {
    console.log(newValue === oldValue) // 可能是 true
  },
  { deep: true },
)

如果你需要比較變化,可能要 watch 具體欄位,或自己建立快照。

Q5:watch 會不會造成無限循環?

會,如果你在 watch 裡修改它正在監聽的 source,而且沒有終止條件。

typescript
watch(count, () => {
  count.value++
})

這會一直觸發自己。實務上要避免這種設計,或加明確條件。


七、 實作題

題目:搜尋頁該用 computed 還是 watch?

需求:

  • 有一份本地 users。
  • 使用 keyword 篩選 users。
  • keyword 改變後,同步 query string。
  • keyword 改變後,向後端搜尋遠端資料。

本地篩選適合 computed:

typescript
const filteredUsers = computed(() => {
  return users.value.filter(user => {
    return user.name.includes(keyword.value)
  })
})

同步 query string 適合 watch:

typescript
watch(keyword, (value) => {
  router.replace({
    query: {
      ...route.query,
      keyword: value || undefined,
    },
  })
})

遠端搜尋也適合 watch:

typescript
watch(keyword, async (value, _oldValue, onCleanup) => {
  const controller = new AbortController()

  onCleanup(() => {
    controller.abort()
  })

  remoteUsers.value = await fetchUsers(value, controller.signal)
})

同一個 keyword,可以同時搭配 computed 和 watch。重點是分清楚:

  • computed 負責同步推導值。
  • watch 負責外部副作用。

八、 資深視角

1. 先問「這是值,還是事情?」

選 API 的第一個問題:

text
我現在要得到一個值?
還是資料變化後要做一件事?

如果是值,用 computed。

如果是事情,用 watch。

這個判斷比背 API 選項更重要。

2. watch 很強,但容易藏資料流

watch 可以做很多事,也因此容易讓資料流變隱性。

例如:

typescript
watch(a, () => {
  b.value = transform(a.value)
})

如果 b 其實只是 a 的衍生值,這段就不如 computed 清楚。

watch 應該用在真的需要副作用的地方。否則系統裡到處都是 watch,會很難追蹤資料如何變化。

3. 副作用要注意清理

watch 常用來接 API、timer、第三方套件。這些都可能需要清理。

好的 watch 不只是能執行 callback,還要考慮:

  • 快速變化時是否有競態?
  • 舊請求是否要取消?
  • 元件卸載時是否清理?
  • 是否需要 debounce?
  • callback 時機應該是 prepost 還是 sync

總結

問題面試回答重點
computed 用途產生同步衍生值
watch 用途監聽變化並執行副作用
快取computed 有快取,watch 不以快取值為目的
lazycomputed 讀取才求值,watch 預設 source 變化才 callback
asynccomputed 不適合,watch 適合
DOM 時機watch 可用 flush: 'post'
選擇原則要值用 computed,要做事用 watch

一句完整的面試回答可以是:

computedwatch 都依賴 Vue 的響應式追蹤,但用途不同。computed 用來描述衍生狀態,它有快取、惰性求值,依賴不變時多次讀取不會重新計算;它應該保持同步且無副作用。watch 用來監聽資料變化並執行副作用,例如 API 請求、同步 localStorage、操作 DOM 或更新路由 query。實務上,如果我要得到一個由狀態推導出的值,用 computed;如果我要在狀態變化後做一件事,用 watch。

延伸閱讀