Masayan tech blog.

  1. ブログ記事一覧>
  2. Vue3のリアクティブシステムを理解する(後編)

Vue3のリアクティブシステムを理解する(後編)

公開日

環境

  • macOS Monterey 12.0.1
  • VSCode
  • node.js v18.6.0
  • npm 8.13.2
  • vue 3.2.25

使用するソースコード

以下の公開リポジトリに置いています。見出し毎にブランチ名を切っているので、ご覧になりたい方は適宜チェックアウトいただければと思います

cloneしたら、npm install && npm run devでビルドしてブラウザで動作確認可能です

また、npmを使う必要があるので、まだの方は先にインストールを済ませてください。

リアクティブな値の更新を検知する

更新を検知する方法

前半の記事で紹介したように、ユーザーの操作によりある値が更新されたら、domを更新するという使い方のほか、ある値の更新を検知して何かの処理を行いたいという場面が多々あります。こういった場合にもリアクティブ性が重要になってきます。

ブランチ名:how-to-detect-reactive-value

  • template上で単にリアクティブな値を表示しているだけであれば、vueがDOMと仮想DOMの差分をレンダリングしてくれるので特に問題はありません。
  • 一方、scriptタグ内でリアクティブな値を参照してなにか処理を行う場合はユースケース毎に関数(method, computed, watch)を使用してその内部で対象のリアクティブな値を参照する必要があります
  • 例えば、画面にTODOリストとその件数が表示されている場合、リストに項目が追加された場合、その変更に伴ってタスクの全件数を再度算出して画面に表示するといった内容です

上記を見ていただけるとユーザーの操作によって、TODOリストが更新されているにもかかわらず、その件数表示が更新されているものとそうでないものがあります。

以下がコード例ですが、todoList がリアクティブな値となっており、notWorkingCount(機能しないカウンター)は、関数を使用せずそのままtodoList のlengthを使ってTODOリストの件数を計算しようとしています。これでは、todoList の変更を検知して動的に算出することはできません。

一方、workingCount1 では、method(単なるjavascriptの関数)が、workingCount2では、computedが使われており、todoList の変更を検知して動的にリストの件数を計算することができるようになっています。

<template>
  <button @click="add">TODOを追加する</button>
  <p>TODOの全件数:{{ notWorkingCount }}</p>
  <p>TODOの全件数:{{ workingCount1() }}</p>
  <p>TODOの全件数:{{ workingCount2 }}</p>
  <ul>
    <li v-for="(todo, index) in todoList" :key="index.toString()">
      {{ todo.title }}{{ todo.isComplete ? "完了" : "未完了" }}</li>
  </ul>
</template>

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

export default {
  setup() {
    const todoList = ref([]);

    const add = () => {
      const todo = {
        title: "洗濯",
        isComplete: false,
      };
      todoList.value.push(todo);
    };

    const notWorkingCount = todoList.value.length; // NG

   const workingCount1 = () => {
     return todoList.value.length;
   }; // method

   const workingCount2 = computed(() => {
     return todoList.value.length;
   }); // computed

・・・
},
};
</script>

どのように関数を使い分ればいい??

ブランチ名:proper-use-function

methods(vue3からだと、厳密にはmethodsはないので、ただの関数), computed(算出プロパティ), watchの使い分けについては、大きく以下の通りです(ここは論争があるので、一概にこれが正解であるとは言えません)

  • パフォーマンスの面で、依存関係(関数内で参照する値)のキャッシュが効くcomputedを第一優先として使用する
    • methodはmethod内で使用しているリアクティブな値が変更されていなくても、他のリアクティブな値が変更されると再評価される(=無駄な処理)
    • computedはキャッシュが効くため、computed内で使用しているリアクティブな値が変更されない限り、再評価されない
  • watchはcomputedでは処理できない複雑な処理や非同期処理、返り値のない処理を実装したい場合に使用する
  • methodはイベントリスナー(@clickなど)や上記2種類の関数で対応できない場合に使用する

computedのキャッシュ機能について

  • method(workingCount1)とcomputed(workingCount2)でそれぞれconsole.logを追加することで、関数が再生成されるとログが出力されるようにします。
<template>
<p>TODOリストとは無関係のリアクティブな値:{{ other }}</p>
<button @click="updateOther">
TODOリストとは無関係のリアクティブな値を更新する
</button>
<button @click="add">TODOを追加する</button>
<p>TODOの全件数:{{ workingCount1() }}</p>
<p>TODOの全件数:{{ workingCount2 }}</p>
<ul>
  <li v-for="(todo, index) in todoList" :key="index.toString()">
    {{ todo.title }}{{ todo.isComplete ? "完了" : "未完了" }}</li>
</ul>
</template>

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

export default {
  setup() {
    const todoList = ref([]);

    const add = () => {
      const todo = {
        title: "洗濯",
        isComplete: false,
      };
      todoList.value.push(todo);
    };

    const other = ref("other value");
    const updateOther = () => (other.value = "updated other value");

    const workingCount1 = () => {
      console.log("reassessmente method !!");
      return todoList.value.length;
    };
  
    const workingCount2 = computed(() => {
      console.log("reassessmente computed !!");
      return todoList.value.length;
    });
  ・・・
  },
};
</script>
  • TODOを追加するボタンをクリックした際は、どちらのログも出力されており、これは、どちらの関数の内部においてもtodoListというリアクティブな値が使用されており(依存関係にあり)、このリアクティブな値が更新されることによって関数が再生成され、ログが出力されているので想定している動作になっています。
  • しかし、TODOリストとは無関係のリアクティブな値を更新するボタンをクリックすると、computedは、内部で使用しているリアクティブな値が変更されない限り再生成しない(キャッシュ)ので、ログが出力されません。一方、methodはキャッシュ機能がないため内部で使用していない全く無関係なリアクティブな値が更新されるだけでも、関数が再生成されログが出力されています。

propsとリアクティブ

vueを使ってアプリケーションを開発するとなると、propsの使用は避けては通れません。そのため、propsとリアクティブの関係性について以降で紹介します

ブランチ名:reactive-props

  • これまで、App.vueに全て記載していましたが、App.vueと子コンポーネント(TodoList.vue)を使用するようにしてみます
  • TODOリストと、TODOを追加する関数(add)をpropsで子コンポーネント(TodoList.vue)に渡す

App.vue

<template>
  <TodoList :todo-list="todoList" :add="add" />
</template>

<script>
import { ref, onMounted } from "vue";
import TodoList from "./components/Todo/TodoList.vue";
export default {
components: {
  TodoList,
},
setup() {
  const todoList = ref([]);

  const add = (todo) => {
    todoList.value.push(todo);
  };

  // fetch todos
  onMounted(async () => {
    const response = await fetch(
      "https://jsonplaceholder.typicode.com/todos"
    );
    const todos = await response.json();

    todos.forEach((todo, index) => {
      ・・・
      todoList.value.push(todo);
    });
  });

  return {
    todoList,
    add,
  };
},
};
</script>
  • 実は、props自体がProxyオブジェクトのため、ref, reactiveを使用しなくても、propsの値をそのまま使用するだけでtemplateの変数(ここでは、TODOリストの件数)も動的に更新される(=propsはリアクティブな値)
  • 一方、scriptタグ内で、propsの変更を受けて動的にpropsを参照している値を更新したり、一定の処理を行いたい場合は、すでに述べた通り、ユースケースに応じて関数(method, computed, watch)を使う必要があります

propsは複数あることが多いので、以下のようにpropsをそのまま関数内で依存関係として使用することは避けるべきです。(当該関数で使用したいpropsの値とは関係ない値が変更されても、関数が再評価されてしまう)

watch(props, () => { });

propsはproxyオブジェクトなので、toRefやtoRefsを用いて、リアクティブな状態を保持したまま必要な値だけ取り出して関数内で使用するようにすべきです。

※なお、前述のとおり、watchなどの関数内ではリアクティブな値を対象とする必要があるため、以下のようにすると、変更をリアクティブに検知できなくなります

watch(props.hoge, () => { });
  • 下図で件数が0件のままになっている箇所はprops.todoList.length; のように、関数を使用せずにpropsの値をそのまま使用しているため、propsの更新を受けて動的に件数を出すことができないようになっています(※ただし、templateで{{ todoList.length }})とすると、propsの更新を受けて動的に件数を出すことができる。(この部分がややこしい)

TodoList.vue

<template>
  <button @click="add(todo)">TODOを追加する</button>
  <p>TODOの全件数:{{ todoList.length }}</p>
  <p>TODOの全件数:{{ count }}</p>
  <p>TODOの全件数:{{ notWorkingCount }}</p>
  <ul>
    <li v-for="(todo, index) in todoList" :key="index.toString()">
      {{ todo.title }}{{ todo.isComplete ? "完了" : "未完了" }}</li>
  </ul>
</template>

<script>
import { computed } from "vue";

export default {
  // Proxy {todoList: Proxy, add: ƒ}
  props: {
    todoList: {
      type: Array,
      required: true,
    },
    add: {
      type: Function,
      required: true,
    },
  },
  setup(props, context) {
    const todo = {
      userId: 1,
      id: 16,
      title: "cooking",
      completed: false,
    };

    // このように、TODOリストを使用してその数を計算したい場合。propsのtodoList.lengthをそのまま使うと機能しない。関数を使用する必要がある
    const count = computed(() => props.todoList.length);
    const notWorkingCount = props.todoList.length;

    return {
      todo,
      count,
      notWorkingCount,
    };
  },
};
</script>

まとめ

いかがでしたでしょうか。本記事では、Vue3のリアクティブ性について考察をしています。リアクティブとは何なのかどういうときに使用するのか、使用する際にはどのようなことを気をつければよいのか等について、具体的なコード例を交えながら説明しています。