🤖

Vueのcomputedをゆるく解説してみる

2024/05/27に公開

🌱 はじめに

Vue.jsを使いたてでよく「結局computedってなんなん...??」「これはcomputedにした方がいい...??」と悩んでいたのでその考え方として。
公式ドキュメントを元にゆるい日本語で説明していきます。
https://ja.vuejs.org/guide/essentials/computed.html

🤔 computedって??

あるデータをウニャウニャして新しいデータを算出する。

import { reactive, computed } from 'vue'

const author = reactive({
  name: 'John Doe',
  books: [
    'Vue 2 - Advanced Guide',
    'Vue 3 - Basic Guide',
    'Vue 4 - The Mystery'
  ]
})

// authorの中のbooks の配列の長さを測ってpublishedBooksMessageという新たなデータを作る
const publishedBooksMessage = computed(() => {
  return author.books.length > 0 ? 'Yes' : 'No'
})

👍 いいこと

再計算が自動でできる

さっきのコードをちょっと増やしてHTML部分も追加して、本を追加・削除するものを作ってみる。

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

const author = reactive({
  name: 'John Doe',
  books: [
    'Vue 2 - Advanced Guide',
    'Vue 3 - Basic Guide',
    'Vue 4 - The Mystery'
  ]
})

const newBook = ref('');

const publishedBooksMessage = computed(() => {
  return author.books.length > 0 ? 'Yes' : 'No'
})

function addNewBook() {
  author.books.push(newBook.value);
  newBook.value = '';
}

function removeBook(index) {
  author.books.splice(index, 1);
}
</script>

<template>
  <div>
    <p>Has published books:</p>
    <span>{{ publishedBooksMessage }}</span>
    <ul>
      <li v-for="(book, index) in author.books" :key="index">
        {{ book }}
        <button @click="removeBook(index)">Remove</button>
      </li>
    </ul>
    <input type="text" v-model="newBook" />
    <button @click="addNewBook">Add Book</button>
  </div>
</template>

こんな感じのものだとデータが変わりやすいとか画面更新せずに変更したい、みたいな時にとても使える。
computed を使う何よりの理由。

コードが見やすい

先ほどのコードを基に

<p>Has published books:</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>

みたいな対応をしたいとなったら

<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>

にした方がスッキリする。

責務分けできる

さっきと似ているが、HTMLタグ書くところは構成を見せるところ、ロジックは <script></script> 内に書くという役割のすみ分け(= 責務分け)を明確にしてあげる。

<p>Has published books:</p>
<!-- 構成とロジックが一緒になっている -->
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>
<script setup>
import { reactive, computed, ref } from 'vue'

const author = reactive({
  name: 'John Doe',
  books: [
    'Vue 2 - Advanced Guide',
    'Vue 3 - Basic Guide',
    'Vue 4 - The Mystery'
  ]
})

const newBook = ref('');

const publishedBooksMessage = computed(() => {
  return author.books.length > 0 ? 'Yes' : 'No'
})

function addNewBook() {
  author.books.push(newBook.value);
  newBook.value = '';
}

function removeBook(index) {
  author.books.splice(index, 1);
}
</script>

<template>
  <p>Has published books:</p>
  <!-- ロジックをcomputed()内にしてここでは構成だけ -->
  <span>{{ publishedBooksMessage }}</span>
</template>

可読性の向上に加えて、コンポーネントを再利用しやすくなる。
また、条件が変わったときにHTML構成内から探すのではなくて computed から探して書き換える方がやりやすさが上がる。

❓ 通常の変数扱いじゃダメなん??

こんな感じ。

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

const author = reactive({
  name: 'John Doe',
  books: [
    'Vue 2 - Advanced Guide',
    'Vue 3 - Basic Guide',
    'Vue 4 - The Mystery'
  ]
})

const publishedBooksMessage = ref(author.books.length > 0 ? 'Yes' : 'No')
</script>

これ自体は問題なく動くが、あくまでも publishedBooksMessage初期値のauthor.bookの要素数に依存するので、要素が追加・削除されてもリアクティブには変更できないことに注意。

🚀 関数扱いじゃダメなん??

処理っぽい感じで関数として扱ってみたいと思う。

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

const author = reactive({
  name: 'John Doe',
  books: [
    'Vue 2 - Advanced Guide',
    'Vue 3 - Basic Guide',
    'Vue 4 - The Mystery'
  ]
})

const newBook = ref('')

function publishedBooksMessage() {
  return author.books.length > 0 ? 'Yes' : 'No';
}

function addNewBook() {
  if (newBook.value.trim() !== '') {
    author.books.push(newBook.value.trim())
    newBook.value = ''
  }
}

function removeBook(index) {
  author.books.splice(index, 1)
}
</script>

<template>
  <div>
    <p>Has published books:</p>
    <span>{{ publishedBooksMessage() }}</span>
    <ul>
      <li v-for="(book, index) in author.books" :key="index">
        {{ book }}
        <button @click="removeBook(index)">Remove</button>
      </li>
    </ul>
    <input type="text" v-model="newBook"/>
    <button @click="addNewBook">Add Book</button>
  </div>
</template>

https://ja.vuejs.org/guide/essentials/computed.html#computed-caching-vs-methods にも書いてあるように実はこれ、 computed とした時と同じように動かせる。
その上で公式ドキュメントは↓を computed のセールスポイントとしている。

算出プロパティはリアクティブな依存関係にもとづきキャッシュされる

???

ということで別のコードで考えてみる。

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

const count = ref(0)

const computedResult = computed(() => {
  console.log('Computed calculated');
  return count.value * 2;
})

function calculateComputed() {
  // あえて同じ値を入れることでcomputedを発動しようとしてみる
  count.value = count.value;
}

function methodResult() {
  console.log('Method calculated');
  return count.value * 2;
}
</script>

<template>
  <div>
    <input type="number" v-model="count" />
    <button @click="calculateComputed">Run Computed</button>
    <button @click="methodResult">Run Method</button>
    <p>{{ computedResult }}</p>
    <p>{{ methodResult() }}</p>
  </div>
</template>

みたいなコードがあった時に
inputタグに値を入れた状態で
Run Computed ボタンを連打 → console.log() が動かない
Run Method ボタンを連打 → console.log() が動く

ということっぽい。
関数にすると値が変わっていなくても都度処理が走るので
重くなってしまう部分をcomputedにするということも利点の1つとのこと。

🔧 getter関数とsetter関数

computed にはgetter関数とsetter関数という概念が存在する。
getter → getなので値を取得する
setter → setなので値を設定する
という役割がある。

もっと砕いていうと
getter → そのcomputedの値を算出する
setter → computedの値から他の変数などを代入したり再計算したりする
って感じみたい。

今までのように特に明示しない書き方はgetter関数とみなされている。

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

const firstName = ref('');
const lastName = ref('');

const fullName = computed({
  // getter 関数
  get() {
    console.log('Getter called');
    return firstName.value + ' ' + lastName.value;
  },
  // setter 関数
  set(value) {
    console.log('Setter called');
    const names = value.split(' ');
    firstName.value = names[0];
    lastName.value = names[1];
  }
})
</script>

<template>
  <div>
    <p>名字: {{ lastName }}</p>
    <p>フルネーム: {{ fullName }}</p>
    <input v-model="lastName" placeholder="Enter your last name" />
    <input v-model="fullName" placeholder="Enter your full name" />
  </div>
</template>

みたいなコードがあった場合に、 firstName や lastName に変更が入ると今まで通り fullName のgetter関数が動いて表示される。
加えて fullName 側にも変更が入ると firstNamelastName に変更が入る。

💡 computedの使い方

ここ にはベストプラクティスと書いていたけどあくまでも使い方として。

computed は他の変数などの書き換えついでに変わってしまう値(公式では「派生した状態」「スナップショット」って言ってる)。
getter関数内では他の変数を書き換えたりDOM操作したりはしたらダメだよ〜ということ。

じゃあどうするの??というのはwatchを使ってねとのこと。
※ setter関数で書く場合は問題なさそう

computed(getter)で他要素の操作はNG
<script setup>
import { reactive, computed } from 'vue'

const author = reactive({
  name: 'John Doe',
  books: [
    'Vue 2 - Advanced Guide',
    'Vue 3 - Basic Guide',
    'Vue 4 - The Mystery'
  ]
})

const publishedBooksMessage = computed(() => {
  author.books.value.push('Vue 5 - Cartoon');
  return author.books.length > 0 ? 'Yes' : 'No'
})
</script>

また、 computed は他の要素の書き換えついでに変わってしまう値なので computed を直に書き換えることもNG。
元の書き換え要素を書き換えてね。

computedを直接書き換えることはNG
<script setup>
import { reactive, computed } from 'vue'

const author = reactive({
  name: 'John Doe',
  books: [
    'Vue 2 - Advanced Guide',
    'Vue 3 - Basic Guide',
    'Vue 4 - The Mystery'
  ]
})

const publishedBooksMessage = computed(() => {
  return author.books.length > 0 ? 'Yes' : 'No'
})

function addNewBook() {
  publishedBooksMessage.value = 'Yes';
}
</script>
Arsaga Developers Blog

Discussion