はじめに
Vue.js(Composition API)では、watch関数を使用することで、リアクティブな状態の一部が変更されるたびにコールバックを実行することができます。
const a = ref(0)
watch(a, (newVal, oldVal) => {
console.log(newVal, oldVal)
})
コールバック関数の第一引数(newVal)には、変更後のaの値が入り、第二引数(oldVal)には変更前のaの値が入ります。
つまり、上記のリアクティブな値aに1を代入した場合、newValには1が入り、oldValには0が入ることになります。
本題
リアクティブなオブジェクトをwatchで監視する場合、下記のようになります。
const obj = ref({ a: 1, b: 2 })
watch(
obj,
(newVal, oldVal) => {
console.log(newVal === oldVal);
// => true
},
{ deep: true }
);
deep: trueをつけることで、オブジェクトのプロパティの変更時にも、watchが変更を検知してくれるようになります。
しかしここで問題があります。
リアクティブなオブジェクトをwatchで監視する場合、コールバック関数の引数である、newValとoldValの値が同じになってしまいます。
つまり、上記でobjのプロパティaに3を代入した場合、newValとoldValの値はどちらも{ a: 3, b: 2 }になります。(どちらもnewValの値になる)
原因として、JavaScriptではオブジェクト型の場合、プリミティブ型と違って値自体ではなく、参照をコピーするため、結果的に同じオブジェクトを参照するからです。
解決方法
解決方法としては、スプレッド構文で別の参照のオブジェクトをつくり、その値を返すgetter関数をwatchの第一引数に指定することです。
const obj = ref({ a: 1, b: 2 })
watch(
() => ({ ...obj.value }),
(newVal, oldVal) => {
console.log(newVal === oldVal);
// => false
},
{ deep: true }
);
スプレッド構文を使うことで、オブジェクトを展開できるため、別の参照のオブジェクトをつくることができます。
1点注意点なのが、スプレット構文はディープコピーではなくシャローコピーになるので、階層の深いオブジェクトの場合は、一旦JSONにして戻したり、structuredCloneやlodashのcloneDeepを使ってディープコピーで別の参照のオブジェクトを作る必要があります。
import { cloneDeep } from "lodash";
const obj = ref({ a: 1, b: { c: 1 } })
watch(
() => ({ ...obj.value }),
(newVal, oldVal) => {
console.log(newVal.b === oldVal.b);
// => true
},
{ deep: true }
);
watch(
() => cloneDeep(obj.value),
(newVal, oldVal) => {
console.log(newVal.b === oldVal.b);
// => false
},
{ deep: true }
);
続いて、下記のようにオブジェクトのみを返すアロー関数の場合、カッコ()で囲むことで、returnと外側の中括弧{}を省略することができます。
// 省略なし
() => { return { ...obj.value }}
// 省略
() => ({ ...obj.value })
そして、watchではリアクティブなデータソースであれば、第一引数には下記の4パターンで定義することができます。
- refやcomputed
- リアクティブなオブジェクト
- 値を返すgetter関数
- 上記3つのいずれかの配列
const x = ref(0)
const y = ref(0)
const obj = ref({ a: 1, b: 2 })
// refやcomputed
watch(x, () => {
// 処理
})
// リアクティブなオブジェクト
watch(obj, () => {
// 処理
})
// 値を返すgetter関数
watch(
() => x.value + y.value,
() => {
// 処理
}
)
// 複数ソースの配列
watch(
[x, y, obj, () => x.value + y.value],
() => {
// 処理
})
値を返すgetter関数を使う場合、ウォッチャーはgetter関数の戻り値が変更されたときだけ起動します。
そのため別の参照のオブジェクトをgetter関数で返すことで、newValとoldValで別々の値がとれるようになります。
最後にもう一度完成コードを載せておきます。
const obj = ref({ a: 1, b: 2 })
watch(
() => ({ ...obj.value }),
(newVal, oldVal) => {
console.log(newVal, oldVal);
},
{ deep: true }
);
もっと良い方法や間違いなどあればご指摘いただけると幸いです。
参考