🐈

Vue watch(watchEffect)使用法

2024/10/26に公開

では今回はwatchEffectを解説します。

前回のcomputed()は処理を一つにまとめるための方法で、それを実現するために内部のリアクティブなデータを監視してそれらが変更した時に関数を再実行するというシステムでした。

そういう何かが変更されたら何かの処理をするという機能はそれ単体でとても便利で、computed()みたいに処理をまとめたいわけではないけど、この機能が使いたいというときにwatchEffectを使用します。

watch(watchEffect)を簡単に言うとcomputed()とはまた別に変更を検知して何か処理をすると言うことに特化した機能になります。

この機能は2種類あってwatchとwatchEffectと言い、両方ともvueからインポートして使用します。
このうちのどちらかを使用しますが、まずはwatchEffectから解説します。

watchEffectもref()などと同じように関数になっていて使い方はcomputed()と同じように実行中にアクセスしたリアクティブなデータを一時的に監視して、それらが変更されたらまたこの関数が再実行されて、またその時にアクセスしたリアクティブなデータを監視して、またその変更を待機するという動きを繰り返すようになります。
言葉だとよくわからないので実際にコードを書いてみます。

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

const count = ref(0)
watchEffect(() => {
  console.log('watchEffect')
  console.log(count.value)
})
</script>

<template>
  <p>{{ count }}</p>
  <button @click="count++">+1</button>
</template>

上記のようにcountの定数を作り、<template>でcountボタンを設定してボタンを押すとカウント+1する処理を書きます。
そうすると最初に一度watchEffectがすぐに実行されますが、その時にcount.valueが監視されるようになり、常に使われるので+1押す度にこのwatchEffectはカウントを監視し続けることになります。

そしてwatchEffectは副作用も入れることができるので非同期処理のsetTimeoutも使えますし、count.valueを更新するような処理も書くことができます。

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

const count = ref(0)
watchEffect(() => {
  console.log('watchEffect')
  console.log(count.value)
  setTimeout(() => {
    console.log('after 1 sec')
  }, 1000)
  count.value = 'hello'
})
</script>

二つ注意点ですが、watchEffectやcomputed()が変更監視するのは関数を実行した時にアクセスしたリアクティブなデータのみです。
つまり、データを取得して読み取っている必要がありますので、もちろんwatchEffect内でリアクティブなデータが無い場合は監視されません。
もう一つ、このwatchEffectの内部では同期的な処理しか実行するすることができません。
例えば、上記のようにwatchEffect内のsetTimeout内にリアクティブなデータを入れても監視されませんのでリアクティブなデータを監視したい場合は必ず同期的(この関数が実行されている間)にそのデータにアクセスする必要があります。

次にwatchです。
watchもwatchEffectと同じように関数になっていてimportして呼び出しして使うことができます。
watchはwatchEffectと違い、引数を2つ取ります。
1つ目の引数に監視したいリアクティブなデータを入れます。
第2引数にその監視しているデータが更新された時に実行したい処理を関数で書きます。

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

const count = ref(0)
watch(count, ()=> {
  console.log('watch')
})
</script>

<template>
  <p>{{ count }}</p>
  <button @click="count++">+1</button>
</template>


こうするとcountが更新されたらwatchが出るというふうになります。
watchとwatchEffectの違いは何かというと、明示的に監視したいデータを指定するかどうかです。
watchはwatchEffectと違い、内部で使っているデータを全部監視するわけではありません。
わかりやすく見るためにリアクティブなデータを増やしてみます。

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

const count1 = ref(0)
const count2 = ref(0)
const count3 = ref(0)
watch(count1, () => {
  console.log('watch')
  console.log(count1.value, count2.value, count3.value)
})
watchEffect(() => {
  console.log('watchEffect')
  console.log(count1.value, count2.value, count3.value)
})
</script>

<template>
  <p>{{ count1 }}(count1)</p>
  <p>{{ count2 }}(count2)</p>
  <p>{{ count3 }}(count3)</p>
  <button @click="count1++">count1++</button>
  <button @click="count2++">count2++</button>
  <button @click="count3++">count3++</button>
</template>

console.logを見るとわかるかと思いますが、watchはcount1を押した時のみ出力されていて、watchEffectはどのボタンを押しても出力されます。

watchは今はcount1しか監視していないからですね。
このような違いがあります。

なので基本的には監視対象を明示的に書いたり限定させたい時はwatch、
コードを短く書きたい時やまとめて使用したいときはwatchEffectを使うという使い分けをするといいでしょう。

もちろんwatchも副作用を入れれますのでsetTimeoutなども入れることができますし、外部のデータも取ることができます。

もう少し詳しくwatchについて解説します。
watchの第2引数の関数には引数を2つ取ることができまして、そのうちの1つ目にはこの第1引数で監視しているデータの最新の値が入り,第2引数には古い値(変更前の値)が入ります。
下記のようにconsole.logで出力してみてみましょう。

watch(count1, (newValue, oldValue) => {
  console.log('watch')
  console.log('newValue', newValue)
  console.log('oldValue', oldValue)
})


このように更新された値がnewValueで古い値がoldValueに格納されていて、データが更新された時に両方とも値が更新されているのがわかります。
このように引数も取れて古い値と新しい値を取ることができます。

次にwatchを使う上での注意点ですが今回の場合、count1に.valueは付けないで下さい。ここで.valueを付けてしまうと第1引数に0を指定したのと同じ意味になってしまうからです。
なので必ずwatchの第1引数にはref()かcomputed()かreactive()を直接入れてください。(実際の値は入れない)
もしくは、この第1引数には関数を入れることができるようになっていて、下記のように書いた場合はwatchEffectと同じような動きをします。

watch(
  () => {
    console.log('watch first')
    return count1.value
  },
  (newValue, oldValue) => {
    console.log('watch')
    console.log('newValue', newValue)
    console.log('oldValue', oldValue)
  }
)

こうすると第1引数はwatchEffectと同じような動きをするのでリロードすると最初に console.log('watch first')を出しますし、内部のcount1.valueを監視するのでcountが更新されたらもう一度 console.log('watch first')が実行されます。

この時に一緒に第2引数も実行されていてcount1が更新される度に第2引数のwatchが実行されます。
watchの第1引数には関数を指定することもできるということですね。
その時の(newValue, oldValue)には第1引数の返り値が入り、新しい値と古い値が入るようになります。

この関数を指定した際に一つ注意点があります。
watchでは第2引数のnewValueとoldValueが同じ値であった時は第2引数の関数は実行されないという性質を持っています。
例えば今回の例だとreturnを0にしてみます。
返り値が0だと(newValue, oldValue)が同じ0になり、同じ値なら実行されないというのが働いて第2引数が実行されません。

watch(
  () => {
    console.log('watch first')
    count1.value
    return 0
  },
  (newValue, oldValue) => {
    console.log('watch')
    console.log('newValue', newValue)
    console.log('oldValue', oldValue)
  }
)


第2引数の(newValue, oldValue)の console.logは出力していません。
毎回関数を実行させたい場合は必ず監視するデータが返り値に影響するように書きましょう。

また、もちろん関数なので複数のデータを書くことができます。
例えば、下記のようにcount1かcount2のいずれかのボタンが押された場合、count1かcount2が押された場合は第2引数も実行されますが、count3が押された時は第2引数は実行されません。

watch(
  () => {
    console.log('watch first')
    return count1.value + count2.value
  },
  (newValue, oldValue) => {
    console.log('watch')
    console.log('newValue', newValue)
    console.log('oldValue', oldValue)
  }
)

複数のリアクティブなデータを監視したい場合は配列を第1引数に指定することもできます。

watch([count1, count2], (newValue, oldValue) => {
  console.log('watch')
  console.log('newValue', newValue)
  console.log('oldValue', oldValue)
})


この方が簡潔でわかりやすいですし、先ほどと同じ挙動になりますので複数のデータを監視したい場合は配列の方がいいかと思います。
その時の(newValue, oldValue)は配列で指定した順番と同じようになっているので console.logで見るとわかりやすいですが、新しい値と古い値が配列に格納されています。
また、この配列には先ほどと同じように関数を指定することができます。

watch([count1, () => {
  return count2.value
}], (newValue, oldValue) => {
  console.log('watch')
  console.log('newValue', newValue)
  console.log('oldValue', oldValue)
})


この2つ目の要素は関数の返り値が入るようになりますし、先程同様に返り値が同じだった場合(newValue, oldValue)が同じ時は第2引数の関数は実行されなくなりますので監視したいデータは返り値に影響するように書いてください。

ちなみにアロー関数式で一つの式を書く場合はreturnを省略して書くこともできるのでこのように簡潔に書けます。

watch([count1, () => count2.value], (newValue, oldValue) => {
  console.log('watch')
  console.log('newValue', newValue)
  console.log('oldValue', oldValue)
})

では、今回みたいに0みたいな普通の数字ならどの方法使ってもいいのですが、例えばリアクティブオブジェクトで特定の値のみを監視したい場合はどうするのか?
下記のようにcount3をref()でオブジェクトを作ります。

const count1 = ref(0)
const count2 = ref(0)
const count3 = ref({
  a: 0
})
watch(count3.value.a, (newValue, oldValue) => {
  console.log('watch')
  console.log('newValue', newValue)
  console.log('oldValue', oldValue)
})

watch(count3.value.a~この書き方は良くなくて、これだと0を指定したのと同じ意味になってしまうのでこの書き方はできません。
なのでこういうリアクティブオブジェクトのプロパティを監視したい場合は下記のように必ず関数を使った書き方で指定する必要があります。

watch(
  () => count3.value.a,
  (newValue, oldValue) => {
    console.log('watch')
    console.log('newValue', newValue)
    console.log('oldValue', oldValue)
  }
)

あとはwatchが実行されるタイミングですが、一番最初の関数が実行される前に第1引数が読み取られます。
下記のように console.log('watch first')がリロードと同時に出ます。

watch(
  () => {
    console.log('watch first')
    count3.value.a
  },
  (newValue, oldValue) => {
    console.log('watch')
    console.log('newValue', newValue)
    console.log('oldValue', oldValue)
  }
)


第2引数に関しては最初の実行時には出ないようになっていて、値が更新された時にwatchが console.log('watch first')の後に出るということです。

watch(
  () => {
    console.log('watch first')
    return count3.value.a
  },
  (newValue, oldValue) => {
    console.log('watch')
    console.log('newValue', newValue)
    console.log('oldValue', oldValue)
  }
)
<template>
  <p>{{ count1 }}(count1)</p>
  <p>{{ count2 }}(count2)</p>
  <p>{{ count3 }}(count3)</p>
  <button @click="count1++">count1++</button>
  <button @click="count2++">count2++</button>
  <button @click="count3.a++">count3++</button>
</template>

なので、watchEffectは最初にすぐに実行されるけどwatchの第2引数は最初は実行されずに監視しているデータが更新された時だけ実行されるということです。
その上で、この設定を変更する方法もありまして、第3引数に{immediate: true}オブジェクトを取ります。

watch(
  () => {
    console.log('watch first')
    return count3.value.a
  },
  (newValue, oldValue) => {
    console.log('watch')
    console.log('newValue', newValue)
    console.log('oldValue', oldValue)
  },
  {
    immediate: true
  }
)

こうすることでwatchEffectと同じタイミングで実行されるようになり、リロードすると最初にwatchが実行されます。
(この場合、oldValueはundefinedになります)

長々と説明してきましたが使いこなすとかなり便利なので使ってみてください。

次回はclass属性の使い方です。

Discussion