vue.js

【Vue.js】リアクティブシステムについて理解する。

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

今回の記事では、Vue.jsのリアクティブシステムの仕組みについて記事にしていきたいと思います。最近、Nuxt.js/vue.jsを使用したアプリケーションに関わる機会が多くなってきましたので、そのテーマに関するアウトプットが増えていきそうです。

リアクティブの基礎

リアクティブであるということは、「その値を常に監視して、値の変更を検知すること」を指します。Vue.jsではcompoentが保持している値の変更が検知された場合、その変更ぶんをViewに反映させる機能が用意されており、その状態を作るために値を監視し変更を検知するリアクティブシステムが必要になります。

もしリアクティブシステムがなかった場合、値が変更されたとしてもその値が反映されることはありません。

/*
 * リアクティブシステムが適応されていない場合
 */

let value1 = 3;
let value2 = 5;
let sum = value1 + value2

console.log(sum) // 8が出力される。

let value2 = 8; // 格納する値を変更 
console.log(sum) // 8が出力されたまま。

APIから動的に値を読み込んだり、ユーザー起因のイベント発火時に値の状態を変更して、動的にViewを変更させるためにはリアクティブシステムが必須になるというわけです。

リアクティブシステムの内部的な話はVue.jsの公式ドキュメントに記載されいます。

JavaScriptには値が読み込まれたときに追跡、検知し最初に値を読み込んだコードを再実行する仕組みが存在しない為、createEffectやProxcyでラップすることで、設定したオブジェクトとのやりとりを傍受しているようです。

DOMの非同期更新

リアクティブな値を更新した場合、DOMは自動的に更新されます。しかしDOM 更新は非同期で更新される為、データの変更を検知しても、すぐにビューに反映される訳ではありません。無駄なオーバーヘッドを避けるため、関連する処理や変更を全て確認した上で、最終的な結果をビューに反映することになります。ビューへの反映を待つ場合は、nextTickメソッドを使用します。このメソッドはDOM 更新をする非同期関数を全て実行した後に、第1引数として渡された関数を実行させることが可能です。戻り値はPromiseです。

function nextTick(callback?: () => void): Promise<void>

<template>
  <button @click="increment">{{ count }}</button>
</template>
<script>
import { ref, nextTick } from 'vue';

setup() {
  const count = ref(0);
  const increment = async () =>  {
  count.value++;

  // DOM はまだ更新されていない
  console.log(document.getElementById('counter').textContent); // 0

  await nextTick();
  // ここでは DOM が更新されている
  console.log(document.getElementById('counter').textContent); // 1
}
return increment
}
</script>

reactivity内部のソースコードを確認する場合は、下記記事をに参考にすると良いと思います。

reactive

リアクティブな状態を作る為には、reactiveメソッドやrefメソッドを使用します。
まずはreactiveメソッドから確認していきます。

function reactive<T extends object>(target: T): UnwrapNestedRefs<T>

reactiveメソッドを使用して、リアクティブなオブジェクトを作成し、setup()関数で宣言します。専用のメソッドを作成し、templateに登録します。もしreactiveを宣言していない場合は、buttonをクリックしても値が変化することはありません。

<template>
  <button @click="increment">
    {{ state.count }}
  </button>
  <button @click="decrease">
    {{ state.count }}
  </button>
</template>

<script>
import { reactive } from 'vue';

export default {
  setup() {
    //  reactive宣言
    const state = reactive({ count: 0 });

    const increment = () => {
      state.count++;
    };

    const decrease = () => {
      state.count--;
    };

    return {
      state,
      decrease,
      increment,
    };
  },
};
</script>

ネスト

リアクティブな値はdeep(ネストされたオブジェクトも監視するためのオプション)で追跡されます。その為、ネストしたオブジェクトが変化した場合でも、変更を検知することができます。

<template>
  <button @click="increment">
    {{ state.counts.count }}
  </button>
  <button @click="otherPush">Push</button>
</template>

<script>
import { reactive } from 'vue';

export default {
  setup() {
    //  reactive宣言
    const state = reactive({
      array: ['foo', 'bar'],
      counts: { count: 0 },
    });

    const increment = () => {
      state.counts.count++;
    };

    const otherPush = () => {
      console.log(state.array);
      state.array.push('red');
    };

    return {
      state,
      otherPush,
      increment,
    };
  },
};
</script>

もしネストしたオブジェクトをリアクティブな値にしたくない場合は、shallowReactiveメソッドを使用します。reactiveメソッドのshallowバージョンです。

const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2
  }
})

// リアクティブな値であるため変更を検知する。
state.foo++

// ネストされたオブジェクトはリアクティブな値ではない。
isReactive(state.nested) // false

// リアクティブな値ではない。
state.nested.bar++

reactiveのデメリット

分割代入

reactiveメソッドの場合、分割代入をしてしまうと、リアクティブな値でなくなるといったデメリットがあります。

  setup() {
    //  reactive宣言
    const state = reactive({ count: 0 });

    // 分割代入
    const { count } = state

    const increment = () => {
      count++; // 0のまま
    };

    const decrease = () => {
      count--; // 同じく0のまま
    };

もし分割代入を行う場合はtoRefメソッドを使用して、refに変換してから使用する必要があります。

  setup() {
    //  reactive宣言
    const state = reactive({ count: 0 });

    // 分割代入& refに変換する。
    const { count } = toRef(state)

    const increment = () => {
      count++; 
    };

    const decrease = () => {
      count--; 
    };

オブジェクト型

reactiveメソッドは(オブジェクト、配列、およびMapSetなどの コレクション型)に対してのみリアクティブ化が機能します。文字列、数値などのプリミティブ型に対しては機能しません。プリミティブ型をリアクティブ化したい場合は後述のrefメソッドを使用する必要があります。

ref

次にrefメソッドについて記載していきます。refメソッドはプリミティブな値をリアクティブ化することができ、返り値はリアクティブとなります。

export declare function ref<T>(value: T): Ref<UnwrapRef<T>>;

refメソッドでリアクティブ化された値は.valueでアクセスする事が可能です。reactiveメソッドとは異なり、入れ子のプロパティはアンラップされないためです。

const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

ref と reactiveのどちらを使うか問題

個人的な考えですが、コンポーネント内では常にrefメソッドを使用するのが良いと思います。reactiveメソッドの場合、分割代入を行うことができず、toRefメソッドを経由してRefに変換する必要があります。またreactiveメソッドの場合はオブジェクトを監視、refメソッドはプリミティブな値を監視しますが、toRefメソッドを使用すれば、refメソッドでもオブジェクトのプロパティを監視することが可能となります。

refメソッドでできて、reactiveメソッドでできないことが多々あり、リアクティブなデータは refで定義すると決めておいたほうがデータのアクセス方法も統一しやすいと感じます。

■ 参考文献