vue.js

watchとwatchEffectの違いまとめ

マナビト
マナビト
こんにちはマナビトです。
普段はフロントエンドエンジニアとしてNuxt.js/vue.js/TypeScriptを
中心に開発業務を行なっています。

watchについて

watchについては以前記事で触れており、状態の変更に応じて「副作用」を実行する場合に、監視対象を登録して関数を実行します。

■ 公式ドキュメント

watchEffectとは

watchEffectメソッドはwatchと同様に定義したデータを監視し、データに変更があれば登録した処理を実行します。


■ 公式ドキュメント

watchEffectの型

function watchEffect(
  effect: (onCleanup: OnCleanup) => void,
  options?: WatchEffectOptions
): StopHandle

type OnCleanup = (cleanupFn: () => void) => void

interface WatchEffectOptions {
  flush?: 'pre' | 'post' | 'sync' // 初期値: 'pre'
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
}

type StopHandle = () => void

watchEffectは第一引数に実行してほしい処理を行う関数を記述します。第二引数は省略可能で、デバック機能やflushが設定可能です。デフォルトでflush: 'pre'が設定されていて、コンポーネントレンダリングの直前にwatchEffect処理が実行されます。flush: 'post'に設定することでコンポーネントレンダリングの直後にwatchEffect処理が実行されるように変更可能です。リアクティブデータが変更された直後に処理を実行したい場合はsyncに設定します。

onTrack、onTriggerはwatchEffectをデバッグすることが可能です。

const count = ref(0)

watchEffect(() => console.log(count.value)、 {
  flush: 'post',
  onTrack(e) {
     debugger // count.value が依存関係として追跡されたときにデバッグされる。
  },
  onTrigger(e) {
     debugger // count.value が変更されたときにデバッグされる。
  }
 })

count.value++

余談ですが、自分は普段Nuxt.js × compositionAPIを使用しており、@nuxtjs/composition-apiからwatchEffectをインポートしていますが、この場合、WatchOptionsBaseにonTriggerやonTrackが定義されていない為、エラーが吐かれてしまいます。

interface WatchOptionsBase {
    flush?: FlushMode;
}

declare function watchEffect(effect: WatchEffect, options?: WatchOptionsBase): WatchStopHandle;

watchEffectを停止させる

watchする必要がなくなった場合は、watchEffectを停止させることが可能です。

const stop = watchEffect(() => {})

stop()

watch と watchEffect の違い

個人的に異なると感じた点は以下の通りです。

  • 監視対象の定義方法
  • 初回、データがwatchされるタイミング

監視対象の定義方法

watchの場合は、第一引数に監視対象を記述することが可能ですが、watchEffectの場合は、第一引数に実行してほしい処理を行う関数を記述することになります。

<script setup>
import { ref, watch, watchEffect } from 'vue'


const counts = ref(200)
const totalPrice = ref(300)
let watchResult = 0
let watchEffectResult = 0

watch(counts, () => {
  watchResult = counts.value * totalPrice.value
})
  
watchEffect(()=> {
  watchEffectResult = counts.value * totalPrice.value
})
</script>

<template>
    <h1>watchResult: {{ watchResult }}</h1>
    <h1>watchEffectResult:{{ watchEffectResult }}</h1>
    <p>counts: {{ counts }}</p>
    <p>totalPrice: {{ totalPrice }}</p>
  <input v-model="counts">
  <input v-model="totalPrice">
</template>

上記のようにコードを記述してwatchとwatchEffectの挙動を確認した場合、watchは監視対象のcountsの値が変更されたら関数が動作します。一方でwatchEffectはcountsとtotalPriceのどちらかが変更されれば、関数が動作することになります。ただ結局のところ、watchでも複数監視対象を選択することが可能なので、そこまで大きな違いではないかと思います。

初回、データがwatchされるタイミング

watchとwatchEffectでデータが更新されるタイミングが異なります。

    const counts = ref(200)
    const totalPrice = ref(300)
    let watchResult = 0
    let watchEffectResult = 0

    watch(counts, () => {
      watchResult = counts.value * totalPrice.value
    })

    watchEffect(() => {
      watchEffectResult = counts.value * totalPrice.value
    })

    console.log(`watchの結果:`, watchResult) // watchの結果: 0
    console.log(`watchEffectの結果:`, watchEffectResult) // watchEffectの結果: 60000

watchの場合は、countsの値が変化したタイミングで実行される為、watchの結果は0のままです。一方でwatchEffectの場合、flushがデフォルトの場合はコンポーネントレンダリングの直前に実行される為、即座に関数の結果が返されます。コンポーネントレンダリングのタイミングで結果が欲しい場合はwatchEffectを使用する方が良さそうですね。

調べてみると、公式ドキュメントに記載されていました。

const url = ref('https://...')
const data = ref(null)

// watchの場合、関数を実行して、urlを監視対象にする必要がある。
async function fetchData() {
  const response = await fetch(url.value)
  data.value = await response.json()
}
fetchData() // 関数の実行
watch(url, fetchData) // watchの処理


// watchEffectに書き換えると、データの取得と副作用の即時実行を同時に行なってくれる。
watchEffect(async () => {
  const response = await fetch(url.value)
  data.value = await response.json()
})

APIから何らかのデータを即座に取得して、副作用を実行したい場合は、watchEffectの方が記載するコード量も少なくて便利そうですね。

本日はここまでとなります。最後まで読んで下さりありがとうございました。

■ 参考文献