はじめに
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 }
);
もっと良い方法や間違いなどあればご指摘いただけると幸いです。
参考