Skip to content

Vue watch 前先小等一下

dont-depend-on-watch

先說結論:

DANGER

watch 不是不能用,而是在使用 watch 之前,先想想有沒有其他方案,真的沒有才用 watch

千萬不要為了一時方便讓元件裡滿滿的 watch,因為容易產生難以追蹤的副作用,會讓資料流更加複雜。

口說無憑,讓我們舉個栗子 (。・∀・)ノ🌰。

舉個栗子 (。・∀・)ノ🌰

考慮一個簡單的元件,輸入資料型別可以為 stringnumber,回傳則固定為 number

vue
<script setup lang="ts">
interface Props {
  modelValue?: string | number;
}
const props = withDefaults(defineProps<Props>(), {
  modelValue: 0,
})

const emit = defineEmits<{
  (e: 'update:modelValue', value: number): void;
}>()

const numberValue = ref(props.modelValue)

watch(() => props.modelValue, (value) => {
  numberValue.value = Number.parseFloat(value)
})
</script>

你可能會想問:「阿不是說能不用就不要用,怎麼開場就有 watch?」

這是因為除了 watch 以外,沒有其他方法可以在元件內得知 props.modelValue 發生變更,所以此部分使用 watch 沒問題。

以上程式已經將 props.modelValue 數值同步至 numberValue 中,現在只差將 numberValue 數值 emit 出去部分,直覺上可能會這樣寫:

vue
<script setup lang="ts">
// ...

watch(numberValue, (value) => {
  emit('update:modelValue', Number.parseFloat(value))
})
</script>

在簡單的例子中,這樣寫不會有太大問題,但是若 numberValue 資料較為複雜(例如深層物件、矩陣等等)或者耦合其他邏輯時,這樣寫可能會導致 watch 無限呼叫或其他難以追蹤的副作用。

一般情況下,我們應該可以直接知道 numberValue 的變化來源。

WARNING

⚠ 如果沒辦法得知,應該重新仔細思考設計

如果是 input 的話,我們可以從 @change 事件得知數值變化,所以此例子可以不使用 watch,結果可能會這樣:

vue
<script setup lang="ts">
// ...

function handleUpdate() {
  emit('update:modelValue', Number.parseFloat(numberValue.value))
}

// 或是這樣
function handleUpdate(value: string) {
  emit('update:modelValue', Number.parseFloat(value))
}
</script>

也就是在明確的資料流中呼叫 emit。

TIP

或者使用 input 的 @blur 事件,可以在確保使用者輸入完畢,離開 input 後再觸發,減少不必要的連續觸發。

總結 🐟

  • 設計元件時,資料流越單純越好,以免寫得時候很爽,除錯的時候火葬場
  • watch 是一個很方便的 API,但不要濫用,以免過多副作用難以追蹤

但是如果發現元件內有很多「只能用 watch 才能處理的邏輯」,那就表示你可能耦合了太多邏輯和副作用在單一元件內。

請好好思考拆分元件、重構或者重新探討元件設計。( ‧ω‧)ノ╰(‧ω‧ )