【Vue.js - 4】データの監視と加工 

 
## まとめ
・算出プロパティは結果をキャッシュしてくれる
・データの状態に対して処理をしたいならウォッチャ
・カスタムディレクティブは監視をしながらDOMの操作ができる
・更新後のDOMにアクセスしたいならnextTick


## 1. 算出プロパティで処理を含むデータを作成

算出プロパティ（Computed 処理を含むデータ）

```html:html
<p>{{ width }} の半分は {{ halfWidth }}</p>
```

```js:Vue.js
new Vue({
  el: '#app',
  data: {
    width: 800
  },
  computed: {
    //算出プロパティhalfWidthを定義
    halfWidth: function() {
      return this.width / 2
    }
  }
})
```

算出プロパティの組み合わせ

```html:html
<p>X: {{ halfPoint.x }}</p>
<p>Y: {{ halfPoint.y }}</p>
```

```js:Vue.js
new Vue({
  el: '#app',
  data: {
    width: 800,
    height: 600
  },
  computed: {
    halfWidth: function() {
      return this.width / 2
    },
    halfHeight: function() {
      return this.height / 2
    },
    // 「width × height」の中心座標をオブジェクトで返す
    halfPoint: function() {
      return {
        x: this.halfWidth,
        y: this.halfHeight
      }
    }
  }
})
```

ゲッターとセッター

```html:html
<input v-model.number="width"> {{ width }}
<input v-model.number="halfWidth"> {{ halfWidth }}
```

```js:Vue.js
new Vue({
  el: '#app',
  data: {
    width: 800
  },
  computed: {
    halfWidth: {
      get: function() {
        return this.width / 2
      },
      //halfWidth の2倍の数値を width に代入する
      set: function(val) {
        this.width = val * 2
      }
    }
  }
})
```

算出プロパティのキャッシュ機能
算出プロパティ…リアクティブな依存データに基づき、結果をキャッシュする。キャッシュを再構築するトリガになるのはリアクティブなデータのみ
メソッド…キャッシュされない

```html:html
<!-- 算出プロパティ：キャッシュされるため、何度使用しても同じ結果 -->
<ol>
  <li>{{ computedData }}</li><!-- 0.8205563271901375 -->
  <li>{{ computedData }}</li><!-- 0.8205563271901375 -->
</ol>

<!-- メソッド：キャッシュされない。呼び出すたび変わる -->
<ol>
  <li>{{ methodsData() }}</li>0.13761479619266637
  <li>{{ methodsData() }}</li>0.45429414765683895
</ol>
```

```js:Vue.js
new Vue({
  el: '#app',
  computed: {
    computedData: function() { return Math.random() }
  },
  methods: {
    methodsData: function() { return Math.random() }
  }
})
```

サンプルコード：リストの絞り込み・ソート機能

<!-- ※lodash.min.jsを読み込む -->

```html:html
<div id="app">
  <input v-model.number="budget"> 円以下に絞り込む
  <input v-model.number="limit"> 件を表示
  <button v-on:click="order=!order">切り替え</button>
  <p>{{ matched.length }} 件中 {{ limited.length }} 件を表示中</p>
  <ul>
    <!-- v-forでは最終結果、算出プロパティのlimitedを使用する -->
    <li v-for="item in limited" v-bind:key="item.id">
      {{ item.name }} {{ item.price }}円
    </li>
  </ul>
</div>
```

```js:Vue.js
new Vue({
  el: '#app',
  data: {
    //フォームの入力と紐付けるデータ
    budget: 300,
    //表示件数
    limit: 2,
    //もとになるリスト
    list: [
      { id: 1, name: 'りんご', price: 100 },
      { id: 2, name: 'ばなな', price: 200 },
      { id: 3, name: 'いちご', price: 400 },
      { id: 4, name: 'おれんじ', price: 300 },
      { id: 5, name: 'めろん', price: 500 }
    ]
  },
  computed: {
    //budget以下のリストを返す算出プロパティ
    matched: function () {
      return this.list.filter(function (el) {
        return el.price <= this.budget
      }, this)
    },
    //matchedで返ったデータをlimit件返す算出プロパティ
    limited: function () {
      return this.matched.slice(0, this.limit)
    }
  }
})
```



##2. ウォッチャでデータを監視して処理を自動化する

ウォッチャ

```js:Vue.js
new Vue({
   ...
  watch: {
    監視するデータ: function (新しい値, 古い値) {
       valueが変化したときに行いたい処理
    },
    'item.value': function (newVal, oldVal) {
       //オブジェクトのプロパティも監視できる
    }
  }
})
```

ウォッチャ（オプションあり）

```js:Vue.js
new Vue({
   ...
  watch: {
    list: {
      handler: function (newVal, oldVal) {
         //listが変化したときに行いたい処理
      },
      deep: true,//ネストされたオブジェクトも監視するか
      immediate: true //初期読み込み時にも呼び出すか
    }
  }
})
```

インスタンスメソッドでの登録（オプションなし）

```js:Vue.js
this.$watch(監視するデータ, ハンドラ, オプション（任意）)
this.$watch('value', function(newVal, oldVal) {
   //...
})
```

インスタンスメソッドでの登録（オプションあり）

```js:Vue.js
this.$watch('value', function (newVal, oldVal) {
   ...
}, {
  immediate: true,
  deep: true
})
```

一度だけ動作するウォッチャ（unwatchで解除する）

```js:Vue.js
new Vue({
  el: '#app',
  data: {
    edited: false,
    list: [
      { id: 1, name: 'りんご', price: 100 },
      { id: 2, name: 'ばなな', price: 200 },
    ]
  },
  created: function() {
    var unwatch = this.$watch('list', function () {
      //listが編集されたことを記録する
      this.edited = true
      //監視を解除
      unwatch()
    }, {
      deep: true
    })
  }
})
```

実行頻度の制御p130
フォームの入力などによって監視する値が高頻度で変化する→ウオッチャの実行頻度を制御する

```html:html
<!-- ※lodash.min.jsを読み込む -->
<input type="text" v-model="value">
```

```js:Vue.js
new Vue({
  el: '#app',
  data: {
    value: '編集してみてね'
  },
  watch: {
    //実行から指定ミリ秒がすぎた場合にコールバックを呼び出す
    value: _.debounce(function (newVal) {
       //ここへコストの高い処理を書く
      },
      //valueの変化が終わるのを待つ時間をミリ秒で指定
      500)
  }
})
```

複数の値を監視する

```js:Vue.js
//インスタンスメソッドを使い監視対象を関数にして登録
 this.$watch( function() {
   return [this.width, this.height]
 }, function() {
    //widthまたはheightが変化した時の処理
 })
//オプションに登録する場合は、算出プロパティを監視する
 cpmputed: {
   watchTarget: function() {
     return [this.width, this.height]
   }
 },
 watch: {
   watchTarget: function() { ... }
 }
```

オブジェクト型の古い値との比較方法 p132

フォームを監視してAPIからデータを取得（検索フォームに応用できる）


```html:html
<!-- ※axios.min.jsを読み込む -->
<div id="app">
  <select v-model="current">
    <option v-for="topic in topics" v-bind:value="topic.value">
       {{ topic.name }}
    </option>
  </select>
  <div v-for="item in list">{{ item.full_name }}</div>
</div>
```

```js:Vue.js
new Vue({
  el: '#app',
  data: {
    list: [],
    current: '',
    topics: [
      { value: 'vue', name: 'Vue.js' },
      { value: 'jQuery', name: 'jQuery' }
    ]
  },
  watch: {
    current: function (val) {
      //GitHubのAPIからトピックのリポジトリを検索
      axios.get('https:api.github.com/search/repositories', {
        params: {
          q: 'topic:' + val
        }
      }).then(function (response) {
        this.list = response.data.items
      }.bind(this))
    }
  },
})
```

## 3. フィルタの使い方
```html:html
<!-- Mustacheで使用する場合 -->
<div>{{ 対象のデータ | フィルタの名前 }}</div>
<!-- v-bindで使用する場合 -->
<div v-bind:id=" 対象のデータ | フィルタの名前 "></div>
```

ローカルでフィルタを使う

```html:html
<p>{{ price | localNum }}円</p>
```

```js:Vue.js
new Vue({
  el: '#app',
  data: {
    price: 19800
  },
  filters: {
    localeNum: function (val) {
      return val.toLocaleString()
    }
  }
})
//グローバルでフィルタを使う（全てのコンポーネントから使用できる）
Vue.filter('localNum', function(val) {
  return val.toLocalString()
})
```

フィルタに引数を持たせる

```html:html
<p>{{ message | filter(foo, 100) }}</p>
```

```js:Vue.js
new Vue({
  filters: function(message, foo, num) {
    console.log(message, foo, num)
  }
})
```

複数のフィルタを繋げて使用

```html:html
<p>180 度は {{ 180 | radian | round }} ラジアンだよ</p>
```

```js:Vue.js
new Vue({
  el: '#app',
  filters: {
    //小数点以下を第2位に丸めるフィルタ
    round: function (val) {
      return Math.round(val * 100) / 100
    },
    //度からラジアンに変換するフィルタ
    radian: function (val) {
      return val * Math.PI / 180
    }
  }
})
```

## 4. カスタムディレクティブ
コンポーネントのdirectivesオプションに登録することで、特定のコンポーネント内で使用できる

```html:html
<input type="text" v-focus>
```

```js:Vue.js
new Vue({
  el: '#app',
  directives: {
    focus: {
      //紐付いている要素がDOMに挿入されるとき
      inserted: function (el) {
        el.focus()  //要素にフォーカスを当てる
      }
    }
  }
})
// グローバルへの登録
Vue.directive('focus', {
  inserted: function(el){
    el.focus()
  }
})
```

使用可能なフック

```js:Vue.js
Vue.directive('example', {
  //ディレクティブが初めて要素と紐づいた時
  bind: function (el, binding) {
    console.log('v-example bind')
  },
  //紐づいた要素が親Nodeに挿入された時
  inserted: function (el, binding) {
    console.log('v-example inserted')
  },
  //紐付いた要素を包含しているコンポーネントのVNodeが更新された時
  update: function (el, binding) {
    console.log('v-example update')
  },
  //包含しているコンポーネントと子コンポーネントのVNodeが更新された時
  componentUpdated: function (el, binding) {
    console.log('v-example componentUpdated')
  },
  //紐付いていた要素からディレクティブから削除される時
  unbind: function (el, binding) {
    console.log('v-example unbind')
  }
})

//updateおよびcomponentUpdate：
//コンポーネントの仮想DOMが更新された時に呼び出される
```



フックの引数

| 引数 | 作用 |
|:--|:--|
| el  | ディレクティブが付与されている要素  |
| binding  | バインドされた値、引数、修飾子のオブジェクト  |
| vnode  | 要素に対応するVNode  |
| oldVnode  |  更新前のVNode（updateおよびcomponentUpdatedのみ使用可） |


フックの関数による省略記法
第２引数として関数を渡す：bindとupdateにフックされる

```js:Vue.js
Vue.directive('example', function(el, binding, vnode, oldVnode) {
  //bindとupdateで呼び出される処理
})
```

例）動画の再生を操作する

```html:html
 <div id="app">
   <button v-on:click="video1=true">再生</button>
   <button v-on:click="video1=false">再生</button>
   <video src="movie1.mp4 v-video="video1">再生</video>
   <button v-on:click="video2=true">再生</button>
   <button v-on:click="video2=false">再生</button>
   <video src="movie1.mp4 v-video="video2">再生</video>
 </div>
```

```js:Vue.js
new Vue({
  el: '#app',
  data: {
    video1: false,
    video2: false
  },
  directives: {
    video(el, binding) {
      if (binding.value !== binding.oldValue) {前の処理と比較して処理を行う
        binding.value ? el.play() : el:pause()
      }
    }
  }
})
```

第２引数bindingは次のプロパティを含むオブジェクトになる

| 1 | 2 |
|:-:|:-:|
| arg  | 引数 |
| modifiers | 修飾子のオブジェクト |
| value | 新しい値 |
| oldValue  | 古い値（updateおよびcomponentUpdatedのみ使用可） |


## 5. nextTickで更新後のDOMにアクセスする

```html:html
<button v-on:click="list.push(list.length+1)">追加</button>
<ul ref="list">
  <li v-for="item in list">{{ item }}</li>
</ul>
```

```js:Vue.js
new Vue({
  el: '#app',
  data: {
    list: []
  },
  watch: {
    list: function () {
      //更新後のul要素の高さを取得できない…
      console.log('通常:', this.$refs.list.offsetHeight)
      //nextTickを使えばできる！
      this.$nextTick(function () {
        console.log('nextTick:', this.$refs.list.offsetHeight)
      })
    }
  }
})
```