【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>