Vue.jsとTypesctriptでいろいろなプロジェクトをやってきました。
そこで毎度のことながら忘れて失敗してしまうのが、タイトルにもあるように「変更を追跡する」ことの意識です。
まず、Vue.jsは @Watch
で明示的に変更を検知することができます。
// dataの変更を検知するように指示しています。
@Watch('data', {deep: true , immediate: true})
public dataChange () {
console.log('dataが変更されたよ')
}
これによって、dataの中身が変更されたとき、dataChange ()
のメソッドが呼び出されるようになります。
ちなみにdeep
はtrueにすることでネストしたオブジェクトを検知することができるようになり、
immediate
はこのWatchを含むVueインスタンスが生成された直後に1度Callbackが呼ばれます(変更を検知したとみなします)。
また、そもそもVueはリアクティブなフレームワークなので、全てのコンポーネントはWatcherを持っています。
例えば以下のケースなどでも、Vueは変更を検知してくれます。
<template>
<div>
{{ getData }}
</div>
<button @click="changeData()">
dataの変更
</button>
</template>
<script lang="ts">
export default class Hogehoge extends Vue {
public data: string = 'getterから返す値だよ'
public get getData (): string {
return data
}
public changeData () {
this.data = 'ボタンを押しましたね'
}
}
getData
にはgetterから返される「getterから返す値だよ」が表示されます。
この後、dataの値をボタンを押して変更した場合でも、正常にレンダリングされていることがわかります。
Vueのtemplate構文ではこの変更検知がデフォルトで搭載されているからです。
ですがこの変更を検知することができない!といったケースは、Vueを実装する方々をたくさん悩ませてきたかと思います。
例えば、よくありそうなのが以下のケース。
<template>
<div v-for="animal in animals">
{{ animal }}
</div>
<button @click="shuffleAnimals()">
Animalsの並び順を変更する
</button>
</template>
<script lang="ts">
export default class Hogehoge extends Vue {
public animals: string[] = ['dog', 'cat', 'bird']
public shuffleAnimals () {
this.animals = this.shuffle(this.animals)
}
/** 配列を並び替える処理 */
private shuffle (arr: string[]): string[] {
let len = arr.length
while (len > 0) {
const rnd = Math.floor(Math.random() * len)
const tmp = arr[len - 1]
arr[len - 1] = arr[rnd]
arr[rnd] = tmp
len -= 1
}
return arr
}
}
}
期待される動作としては、画面に表示されている
「dog cat bird」がボタンを押すことでランダムに並べ替えられる、というものです。
ですが、これでボタンを押したとてdog cat birdのまま、順番は一行に変わりません。
データはきちんと変わっているのに……
これは公式にも記載がありますが、配列を検知するには以下の条件があります。
Vue は、配列における次の変更は検知できません:
- インデックスと一緒にアイテムを直接セットする場合、例えば vm.items[indexOfItem] = newValue
- 配列の長さを変更する場合、例えば vm.items.length = newLength
今回のケースだと1に該当する(並び順を変えている)ため、検知できていないようです。
もちろん、直接 animals[0] = 'pig'
も検知されません。
じゃあ今回のケースはどう解消するのかというと、これも公式に記載があります。
vm.items.splice(newLength)
Spliceを使う(か、Vue.setを使う)ことで、解消できるとのことでした。
そのため、以下のようにします。
<template>
<div v-for="animal in animals">
{{ animal }}
</div>
<button @click="shuffleAnimals()">
Animalsの並び順を変更する
</button>
</template>
<script lang="ts">
export default class Hogehoge extends Vue {
public animals: string[] = ['dog', 'cat', 'bird']
public shuffleAnimals () {
// ここを変えます。
this.animals = this.animals.splice(0, this.animals.length, ...this.shuffle(this.animals))
}
/** 配列を並び替える処理 */
private shuffle (arr: string[]): string[] {
let len = arr.length
while (len > 0) {
const rnd = Math.floor(Math.random() * len)
const tmp = arr[len - 1]
arr[len - 1] = arr[rnd]
arr[rnd] = tmp
len -= 1
}
return arr
}
}
}
splice
の引数は一つ目に配列のStart, 二つ目にStartから何個までを消すか(=今回は並び替えるので総取っ替えをします。元の配列の長さを指定することですべて入れ替えることができますね), 三つ目にpushと同じく入れたいString型の文字列です(処理自体は配列を返すので、...
でスプレッドオペレータを呼び出してあげましょう)。
これでボタンを押すことで、正常に変更が検知されるようになりました。
他にもVue.set
など、検知されるようにする方法はありますので公式を参照してみてください。
次はみなさんもお困りの、オブジェクトについてお話しします。