【Vue 3】リアクティビティAPIをより理解する
はじめに
前回の記事で、Options APIと比較しながらComposition APIの基本的な使用方法について説明しました。 Composition API を初めて知る方は、この記事を事前に読んでいただけていると幸いです。
この記事では、reactive
やref
などのリアクティビティAPIについて深掘りしていきます。
reactive
reactiveの実体
reactive
はオブジェクトのリアクティブなコピーを返します。
正確には、対象のオブジェクトをTarget
に設定したProxy
を返します。
オブジェクトとしての使用感は変わりませんが、Proxy
のHandler
によってリアクティブが実現されているようです。
以下の例を見てください。
Proxy
についてはこちらを参照してください。
<script setup>
import { reactive } from 'vue';
const origin = { a: 0, b: 0 };
const copy = reactive(origin);
const incOrgA = () => origin.a++; // リアクティブでない
const incCpyA = () => copy.a++; // リアクティブ
</script>
オリジナルのオブジェクトを変更する場合、値そのものは変わりますがリアクティブには変更されません。
リアクティブに変更する場合は、reactive
オブジェクト(Proxy
)側を変更します。
初期化などでオブジェクトそのものを変更したい場合はObject.assign()
を使用します。
当たり前かもしれませんが、reactive
オブジェクトの実体はProxy
のため直接代入は不可です。
<script setup>
import { reactive } from 'vue';
const origin = { a: 0, b: 0 };
const copy = reactive({ ...origin });
const reset = () => Object.assign(copy, { ...origin });
</script>
またisReactive
によって、対象のオブジェクトがreactive
オブジェクトであるかを検証することができます。
<script setup>
import { ref, reactive, isReactive } from 'vue';
const num = ref(0);
const origin = { a: 0, b: 0 };
const copy = reactive(origin);
console.log(isReactive(num)); // false
console.log(isReactive(origin)); // false
console.log(isReactive(copy)); // true
</script>
リアクティブの範囲
reactive
で生成されたオブジェクトはすべてリアクティブになります。
これは、ネストされたオブジェクトにも適用されます。
<script setup>
import { reactive, isReactive } from 'vue';
const child = { a: 0, b: 0 };
const parent = { a: 0, b: 0, c: child };
const copy = reactive(parent);
console.log(isReactive(copy)); // true
console.log(isReactive(copy.c)); // true
//以下はリアクティブに変更される
copy.a = 1;
copy.c.a = 2;
</script>
また以下のように後に代入したオブジェクトも同じくリアクティブになります。
<script setup>
import { reactive, isReactive } from 'vue';
const child = { a: 0, b: 0 };
const parent = { a: 0, b: 0 };
const copy = reactive(parent);
copy.c = child; //後で代入
console.log(isReactive(copy)); // true
console.log(isReactive(copy.c)); // true
//以下はリアクティブに変更される
copy.a = 1;
copy.c.a = 2;
</script>
reactive
にはもう1つ、shallowReactive
というものがあります。
shallowReactive
で生成されたオブジェクトはリアクティブになりますが、ネストされたオブジェクトはリアクティブになりません。
<script setup>
import { reactive, isReactive } from 'vue';
const child = { a: 0, b: 0 };
const parent = { a: 0, b: 0, c: child };
const copy = reactive(parent);
console.log(isReactive(copy)); // true
console.log(isReactive(copy.c)); // false
//以下はリアクティブに変更される
copy.a = 1;
//以下はリアクティブでない
copy.c.a = 2;
</script>
ref
refの基本
ref
は値またはオブジェクトをリアクティブなオブジェクトとして返します。
ref
オブジェクトの値はvalue
プロパティによってアクセスします。
<script setup>
import { ref } from 'vue';
const val = ref(0);
val.value = 1;
const obj = ref({ a: 0, b: 0 });
obj.value.a = 2;
</script>
ref
オブジェクトであるかは、isRef
によって判定できます。
<script setup>
import { ref, isRef } from 'vue';
const a = 0;
const b = ref(a);
console.log(isRef(a)); // false
console.log(isRef(b)); // true
</script>
また対象の変数がref
オブジェクトかがわからないとき、unref
を用いることで便利に値を取得することができます。
ただ単に isRef(v) ? v.value : v
が関数化されているだけです。
<script setup>
import { ref, unref } from 'vue';
const a = 0;
const b = ref(1);
const aa = unref(a); // 0
const bb = unref(b); // 1
</script>
オブジェクトの扱い
オブジェクトに対してref
を使用した場合、その値(value
)はreactive
オブジェクト(Proxy
)になります。
つまりref
、reactive
どちらを使用しても実体としては同じものが取得できます。
違いはref
でラップされているかだけです。
<script setup>
import { ref, isRef, isReactive } from 'vue';
const obj = ref({ a: 0, b: 0 });
console.log(isRef(obj)); // true
console.log(isReactive(obj.value)); // true
obj.value.a = 1; // リアクティブに変更される
</script>
ref
にはもう1つshallowRef
というものがあります。
これは、オブジェクトをreactive
オブジェクトしてではなく、そのまま値(value
)に設定します。
<script setup>
import { shallowRef, isRef, isReactive } from 'vue';
const obj = shallowRef({ a: 0, b: 0 });
console.log(isRef(obj)); // true
console.log(isReactive(obj.value)); // false
obj.value.a = 1; // リアクティブに変更されない
</script>
使用頻度は少ないとは思いますが、shallowRef
を用いた場合にtriggerRef
を用いることで強制的にリアクティブな変更として扱うことができます。
少し難しいかもしれませんので、以下を見てください。
<script setup>
import { shallowref, watch, triggerRef } from 'vue';
const obj = shallowRef({ a: 0, b: 0 });
watch(obj, (cr, prev) => console.log(cr));
const incObjA = () => {
obj.value.a++;
triggerRef(obj);
}
</script>
上記はwatch
でobj
の変更を検知してコンソールに出力するという簡単な例です。
obj
をref
で定義していれば、obj.value.a++
はリアクティブな変更となり、watch
の処理が実行します。
しかしshallowRef
では、obj.value.a++
はリアクティブな変更ではないため、watch
の処理は実行されません。
これに対し、triggerRef(obj)
を実行することで、強制的にobj
はリアクティブに変更されたことにでき、watch
の処理が実行されます。
ここでは例としてwatch
を使用しましたが、computed
やwatchEffect
もリアクティブに変更されたこととして処理してくれます。
refのアンラップ
reactive
で生成したプロパティにref
で生成した値を設定するとします。
<script setup>
import { ref, reactive } from 'vue';
const val = ref(0);
const obj = reactive({ a: 0, b: val });
const incVal = () => val.value++;
const incObjB = () => obj.b++;
console.log(val.value == obj.b); //true
</script>
obj.b
はval
の変更に対してリアクティブに変更されます。
val
も同様にobj.b
の変更に対してリアクティブに変更されます。
この動きについては感覚的にわかると思います。
注目すべきはobj.b
の参照についてです。
incObjB()
を見るとわかるようにobj.b
はref
で定義された値をセットされたにもかかわらず、value
プロパティを参照していません。
詳細は省きますが、これもProxy
としてそのように実装されているからです。
とりあえず、reactive
のオブジェクト内ではref
のvalue
は不要(アンラップされる)と覚えておきましょう。
プロパティの抽出
reactive
オブジェクトのプロパティを、リアクティブの特性を持ったまま抽出したい場合は、toRef
を使用します。
第一引数に対象のreactive
オブジェクト、第二引数に対象のプロパティ名を指定します。
<script setup>
import { reactive, toRef } from 'vue';
const obj = reactive({ a: 0, b: 0 });
const a = toRef(obj, 'a');
// 互いの変更の影響を受ける
obj.a++; // obj.a = 1, a.value = 1;
a.value++; // obj.a = 2, a.value = 2;
</script>
上記のobj.a
とa
は紐づいているため、互いの変更の影響を受けます。
またすべてのプロパティを上記のように抽出したい場合は、toRefs
を使用します。
<script setup>
import { reactive, toRefs } from 'vue';
const obj = reactive({ a: 0, b: 0 });
const { a, b } = toRefs(obj);
</script>
readonly
readonlyの基本
readonly
はオブジェクトを受け取り、読み取り専用にしたコピーを返します。
読み取り専用のため値の変更はできず、警告がコンソールに表示されます。
<script setup>
import { ref, reactive, readonly } from vue;
const val = ref(0);
const roVal = readonly(val);
val = 1; // val = 1, roVal = 1;
roVal = 2; // これはNG(警告)
const obj = reactive({ a: 0, b: 0 });
const roObj = readonly(obj);
obj.a = 1; // obj.a = 1, roObj.a = 1;
roObj.b = 2; // これはNG(警告)
</script>
実際はreactive
と同様に、対象のオブジェクトをTarget
に設定したProxy
を返します。
読み取り専用は、Proxy
のSetterの実装により実現しているわけです。
また、readonly
オブジェクトは対象のオブジェクトの特性をそのまま持ちます。
<script setup>
import { ref, reactive, readonly, isRef, isReactive } from vue;
const val = ref(0);
const roVal = readonly(val);
console.log(isRef(roVal)); // true
const obj = reactive({ a: 0, b: 0 });
const roObj = readonly(obj);
console.log(isReactive(roObj)); // true
</script>
対象のオブジェクトが読み取り専用であることは、isReadonly
で検証することができます。
<script setup>
import { reactive, readonly, isReadonly } from 'vue';
const obj = reactive({ a: 0, b: 0 });
const roObj = readonly(obj);
console.log(isReadonly(obj)); // false
console.log(isReadonly(roObj)); // true
</script>
適用範囲
readonly
はネストされたオブジェクトにも適用されます。
<script setup>
import { reactive, readonly, isReadonly } from 'vue';
const obj = reactive({ a: 0, b: 0, c: { d: 0 }});
const roObj = readonly(obj);
console.log(isReadonly(roObj)); // true
console.log(isReadonly(roObj.c)); // true
</script>
shallowReadonly
を使用することで、ネストされたオブジェクトを適用外にすることができます。
<script setup>
import { reactive, shallowReadonly, isReadonly } from 'vue';
const obj = reactive({ a: 0, b: 0, c: { d: 0 }});
const roObj = shallowReadonly(obj);
console.log(isReadonly(roObj)); // true
console.log(isReadonly(roObj.c)); // false
</script>