🐱

Vue computed使用法

2024/10/23に公開

さて、今回から徐々に複雑になっていきますので各セクション毎で記事を書いていきたいと思います。

今回はcomputedを使って複雑な式をまとめる方法。
computedはリアクティブシステムを保ったまま処理を一つにまとめる方法です。

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

const score = ref(0)
</script>

<template>
 <p>{{ score > 3 ? 'Good!!!👍' : 'Bad👎' }}</p>
  <p>{{ score }}</p>
  <button @click="score++">+1</button>
</template>

このような三項演算子でボタンを押していって3以上になるとGoodになるというのがあるとします。
この三項演算子が長いので定数にまとめたいなという時に、普通に定数作ってもうまく処理がされません。
実際に作ってみてみます。

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

const score = ref(0)
const evaluation = score.value > 3 ? 'Good!!!👍' : 'Bad👎'
</script>

<template>
  <p>{{ score > 3 ? 'Good!!!👍' : 'Bad👎' }}</p>
  <p>{{ evaluation }}</p>
  <p>{{ score }}</p>
  <button @click="score++">+1</button>
</template>


上の三項演算子は4になるとGoodになっていますが、定数evaluationを作ったらうまくいきません。

なぜうまくいかないのか?
それは

 <p>{{ score > 3 ? 'Good!!!👍' : 'Bad👎' }}</p>

これは式であって

const evaluation = score.value > 3 ? 'Good!!!👍' : 'Bad👎'

これは評価された結果がevaluationに入るからで、この式は評価されたら最初0なのでBadです。
だから文字列のBadがevaluationに入るだけとなり、ずっとBadが表示されます。
これをref関数に入れてもできません。

const evaluation = ref(score.value > 3 ? 'Good!!!👍' : 'Bad👎')

これは先ほどと同じようにref関数の中にBadの文字列を入れただけです。

これを解決してくれるのがcomputedになります。
リアクティブシステムを保った式を作るということで、これはrefと同じでimportして使用します。

使い方は下記のようにref関数のように関数式で組み込みます。
まだ完成ではないですが、一度console.logでevaluationを見てみると、ref関数のように複雑なオブジェクトが入っています。

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

const score = ref(0)
const evaluation = computed(() => {score.value > 3 ? 'Good!!!👍' : 'Bad👎'})
console.log(evaluation);

</script>

<template>
  <p>{{ score > 3 ? 'Good!!!👍' : 'Bad👎' }}</p>
  <p>{{ evaluation }}</p>
  <p>{{ score }}</p>
  <button @click="score++">+1</button>
</template>


で、これはrefオブジェクトとほぼ同じで、<template>でもrefオブジェクトのように扱うことができます。
なのでconsole.log(evaluation.value)のように.valueを付けたり、<template>で使用する場合は.valueが暗黙的に付きます。
ではこの.valueには何が入るのか?これはcomputed()で計算した返り値が.valueに格納されます。
では実際にreturnを付けるcomputed()では内部的に色々処理をしてくれてBadと返ってきます。

import { ref, computed } from 'vue'

const score = ref(0)
const evaluation = computed(() => {
  return score.value > 3 ? 'Good!!!👍' : 'Bad👎'
})
console.log(evaluation.value)
</script>

<template>
  <p>{{ score > 3 ? 'Good!!!👍' : 'Bad👎' }}</p>
  <p>{{ evaluation }}</p>
  <p>{{ score }}</p>
  <button @click="score++">+1</button>
</template>

なのでcomputed()を使って引数に関数を入れてreturnでまとめた処理を返せばいいということです。
このように処理を一つにまとめることができます。

この時の処理を内部ではどうなっているのか簡単に解説します。
まず、このような処理の時にcomputed()では一時的に内容を記録していて、.valueにアクセスしたなっていうチェックが行われています。
他にcomputed()内にリアクティブなデータがあった場合も全てチェックします。

const evaluation = computed(() => {
  nyan.value
  baw.value
  return score.value > 3 ? 'Good!!!👍' : 'Bad👎'
})

そして関数が実行され終えた後もデータを監視し続け、このうちの一つでも何かデータが更新されたらその瞬間にこの関数を再度実行し、その時の返り値でcomputedオブジェクトの.valueの値を更新します。
で、2回目の関数の実行時にもまた同じように実行中にアクセスしたリアクティブなデータをチェックし、監視し続け、変更があったらまた3回目の関数も実行するという、ずっと繰り返すという処理をcomputed()は内部でしています。

簡単にいうとref関数の計算ができて監視し続けるとということですね。

次に、computed()を使う上での注意点です。

1.computed()は読み取り専用です

const evaluation = computed(() => {
  console.log(computed)

  return score.value > 3 ? 'Good!!!👍' : 'Bad👎'
})
evaluation.value = 'Nyan'
console.log(evaluation.value)

このようにevaluation.valueに値を入れようとするとエラーが出ます。

なのでcomputed()は基本的に読み取り専用であり、処理をまとめるために使用するものです。

2.副作用での使用は避ける。
これは関数の外側にある何かしらの状態を変更するような処理のことです。
今で言うと、scoreの値を更新するような処理ですね。

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

const score = ref(0)
const evaluation = computed(() => {
  console.log(computed)
  score.value = 0
  return score.value > 3 ? 'Good!!!👍' : 'Bad👎'
})

console.log(evaluation.value)
</script>

computed()内にscore.value = 0のscore.valueを変更しようとしています。(サイドエフェクトという)
ESLintでもエラー出ますし、なるべくやらない方がいいです。

3.非同期の処理では実行しない。
非同期処理とは今行われている処理が全て実行された後に遅れて実行されるような処理です。
これをcomputed()の中で使用することはできません。
例えば、setTimeout関数ですね。
これも外側の状態を変化しているという扱いになるので副作用となり、使用できません。
(非同期処理は基本的に全て副作用)

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

const score = ref(0)
const evaluation = computed(() => {
  console.log(computed)
  setTimeout(() => {}, 1000);
  return score.value > 3 ? 'Good!!!👍' : 'Bad👎'
})

console.log(evaluation.value)
</script>

computed()は常にデータを綺麗にまとめてreturnで返すくらいの処理をするところぐらいの認識でいいと思います。
(複雑にし過ぎない。軽く計算ができてreturnで返すref関数な感じ)

そしてこのcomputed()内は同期的な行動はできるので、例えば外側に関数を作ってcomputed()に入れると使えますし、computed()内なのでここに入れた関数も常に監視されて下記のように変わらず使用できるということですね。

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

function tmp() {
  return score.value > 3 ? 'Good!!!👍' : 'Bad👎'
}
const score = ref(0)
const evaluation = computed(() => {
  console.log(computed)
  return tmp()
})

console.log(evaluation.value)
</script>


ちなみに、このcomputed()の中の関数も監視し続ける内部的な機能のことをReactiveEffectと言います。

このReactiveEffectはcomputed()以外にも使われていて、一つは次に解説するwatchEffectで、もう一つは<template>で使われています。
<template>の中でどのリアクティブなデータが使われているかをチェックするためにcomputed()と同じくReactiveEffectというシステムが使われています。
computed()の場合は変更を検知したら関数を再実行し、
<template>内の場合は変更を検知したら再レンダリングされるようになっています。
なのでリアクティブなデータが更新されたら自動で見た目が最新の状態になるわけです。
簡単にいうとcomputed()と<template>は同じReactiveEffectのシステムで動いているということです。

先ほど、外側で関数を作ってcomputed()に入れる話をしましたが、実はこの関数をそのまま<template>内で使ってもリアクティブに情報が更新されます。

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

function tmp() {
  return score.value > 3 ? 'Good!!!👍' : 'Bad👎'
}
const score = ref(0)
const evaluation = computed(() => {
  console.log(computed)
  return tmp()
})

console.log(evaluation.value)
</script>

<template>
  <p>{{ score > 3 ? 'Good!!!👍' : 'Bad👎' }}</p>
  <p>{{ evaluation }}</p>
  <p>{{ tmp() }}</p>
  <p>{{ score }}</p>
  <button @click="score++">+1</button>
</template>


なので実はcomputed()を使わなくても単純に関数呼び出しをするだけで同じような機能が作れます。
ではどちらを使えばいいのか、これはcomputed()を使用した方がいいです。
関数呼び出しの場合、再レンダリングごとに毎回関数が呼び出されてしまいます。
computed()の場合はあくまでもその中で使っているリアクティブなデータが更新された時だけ関数が実行されるので、必要な時だけ関数が実行されますが、関数呼び出しの場合は違います。
無闇に再レンダリングするとその度に実行されてしまいます。
では見比べてみましょう。

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

const score = ref(0)
const count = ref(0)

function tmp() {
  return score.value > 3 ? 'Good!!!👍' : 'Bad👎'
}

const evaluation = computed(() => {
  console.log(computed)
  return score.value > 3 ? 'Good!!!👍' : 'Bad👎'
})

console.log(evaluation.value)
</script>

<template>
  <p>{{ score > 3 ? 'Good!!!👍' : 'Bad👎' }}</p>
  <p>{{ evaluation }}</p>
  <p>{{ tmp() }}</p>
  <p>score:{{ score }}</p>
  <p>count:{{ count }}</p>

  <button @click="score++">+1 score</button>
  <button @click="count++">+1 count</button>
</template>

scoreを押した時は

このように両方ともconsole.logに出力されています。
これはscoreによってデータの値が変わるかもしれないからで、一方、countはscoreと一切関係ありません。
しかし、countボタンを押すと実行されてしまいます。

scoreとは関係ないのに関数呼び出しが<template>で書かれているために実行されてしまいます。
<template>で書かれていた場合は再レンダリング毎に実行されるからで、再レンダリングはリアクティブなデータがどれか一つでも変更されたら起こってしまうから。

なので処理をまとめたい時はcomputed()を使う方がいいです。
ただ、関数呼び出しも引数を取れてたまに便利なこともあるので一応覚えておきましょう。

あと、このcomputed()はかなり優秀で、内部で利用しているリアクティブなデータが更新されたからといって毎回関数を実行しているわけではありません。
今回だとevaluationのようなcomputed()とオブジェクトの.valueプロパティが監視されていない場合、つまりReactiveEffectを使用しているcomputed()や<template>、次項のwatchEffectの中で使われていない(監視されていない)のであれば関数を実行しなくなります。
<template>内のevaluationを消してscoreのボタンを押してもcomputed()はアクセスはされてますが実行しなくなるということです。
こういう賢い動きをcomputed()はしてくれます。

動きは少し難しいですが、最適化しやすい関数で効率よく処理を実行できるので使いこなしていきたいですね。

では次回はwatchEffectの解説をしていきます。

Discussion