作りたいものがありすぎる

40歳を過ぎてプログラミングを始めた人の顛末とこれからなど

【輪読会資料】基礎から学ぶVue.js CHAPTER4 データの監視と加工 読書メモ

以下の記事は2019/2/21 コワーキングスペース秋葉原Weeybleで行われる輪読会
[秋葉原] 基礎から学ぶVue.js輪読会 ch4 データの監視と加工 (初心者歓迎!)のための読書メモとなります。
以下の書籍の CHAPTER4 データの監視と加工 のメモです。

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

さすがに輪読担当4回中3回ともなると疲れて来た…。

書籍用のサイトのCHAPTER4記述ページ(サンプルコード有り)

これまでの輪読会資料

CAHPTER 1
【輪読会資料】基礎から学ぶVue.js CHAPTER1 Vue.jsとフレームワークの基礎知識 読書メモ - 作りたいものがありすぎる

CHAPTER 2
【輪読会資料】基礎から学ぶVue.js CHAPTER2 データの登録と更新 - Qiita

CHAPTER 3
【輪読会資料】基礎から学ぶVue.js CHAPTER3 イベントとフォーム入力の受け取り 読書メモ - 作りたいものがありすぎる

さきにまとめを書いて全体を把握

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

リアルタイムにDOMの値を調べて、処理(関数)をして、その結果を画面にリアルタイムに反映できる!という事っぽい。

CHAPTER4 データの監視と加工

SECTION 16 算出プロパティで処理を含むデータを作成する

要は普通に自分で作った関数での処理ができてデータもバインドできるので、リアルタイムな処理が可能という事らしい。

算出プロパティの使い方

functionとあるのでメソッド(関数)っぽいけど、プロパティ(変数・値)
従来散々使ったVueのdata:と並行した値でcomputed:を持たせ、そこに各プロパティを定義した関数を入れる構成
computedはのプロパティ以降は関数で定義する。
マスタッシュ({{ hoge }})等でプロパティとして使える

<p>{{ width }} の半分は {{ halfWidth }}</p>
new Vue({
  el: '#app',
  data: {
    width: 800
  },
  computed: {
    // 算出プロパティhalfWidthを定義
    halfWidth: function() {
      return this.width / 2
    }
  }
})

算出プロパティを組み合わせて使用しよう

算出プロパティを組み合わせたり、配列を使う事もできる

this.プロパティ名で他の算出プロパティで定義したり計算した結果を引っ張って利用するとこができる。かなり普通のプログラムっぽい感じになる。
たぶん、画面の高さや幅を算出して、必要な位置に表示物を出す際なんかに強力に使える感じ?

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

ゲッターとセッター

上の例では算出プロパティの値を代入(ユーザーからの入力等)しても実はエラーになってしまう
しかし、セッターもあるので、入力への対応も可能方法はこんな感じ
先週やった相互に値を影響し合える v-modelを使う!

セッターの例 halfWidthをユーザーフォームにバインディングする

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

セッターを利用するには以下の様にget,setの2つの名前決め打ちのプロパティを定義すること

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

これをデモすると、2つの入力フォームに数値が入っており、片方の値を入れると、もう片方の値に影響を与える事ができる!

halfWidth:プロパティ(内の各function)がしていること
getwidthの値を取得して
sethalfWidthの値に代入する
つまり値を順繰りに回しているループしないか不安になるが、これで値をインタラクティブで双方向に連携ができる

算出プロパティのキャッシュ機能

算出プロパティとメソッド(関数)の違い
算出プロパティ  リアクティブなデータをキャッシュ化してしまう。

<p>算出プロパティ キャッシュ化されるので一度しか処理が走らず同じ値が出力される</p>
<ol>
  <li>{{ computedData }}</li>
  <li>{{ computedData }}</li>
</ol>
<p>メソッド 毎回処理が走る為、値が変わる</p>
<ol>
  <li>{{ methodsData() }}</li>
  <li>{{ methodsData() }}</li>
</ol>
new Vue({
    el: '#app',
    computed: {
        computedData: function () { return Math.random() }
    },
    methods: {
        methodsData: function () { return Math.random() }
    }
})

ここがわかった上での使い分けが大事

リストの絞り込みに利用しよう

算出プロパティは内のfunctionは一度だけ処理されキャッシュ化されるので、複雑な処理に向いている
例えば絞り込みリスト等を作る際に便利

<div id="app">
  <input v-model.number="budget"> 円以下に絞り込む
  <input v-model.number="limit"> 件を表示
  <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>
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)
        }
    }
})

一回読み込んだ、mached処理のキャッシュの上にlimitedの処理が乗ってlimitedのみで算出の修正が行われている事になるらしい。

ソート機能を追加しよう

さらに書籍には 上記にソート機能を追加したコードがのっているが、最初動かなかった。 _.orderByってのが怪しんで、最初はタイポで本来どう書けばいいのか悩む。JSにはない関数でVue.jsの1.X系にはある?でも _.って何をしている事なのか不明だったが、よく見るとLodash入れろとあった…。だめじゃん、俺

ということでLodashを入れる。この1行追加

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.5/lodash.min.js"></script>
  <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>
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)
    },
    // sortedを新しく追加   この _.orderBy ってのが Lodash のメソッドらしい
    // https://lodash.com/docs/#orderBy
    sorted: function() {
      return _.orderBy(this.matched, 'price', this.order ? 'desc' : 'asc')
    },
    // limitedで使用するリストをsortedに変更
    limited: function() {
      return this.sorted.slice(0, this.limit)
    }
  }
})

Lodashの_.orderBy関数。

_.orderBy(collection, [iteratees=[_.identity]], [orders])

今回の例の第三引数はthis.order ? 'desc' : 'asc'三項演算子としている。

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

ウォッチャとは特定のデータ、算出プロパティの状態を監視して、データに変化があった際に処理を実行してくれるフック機能のこと
ウォッチャ自体は値を返さない

ウォッチャの使い方

コンポーネントwatchを用意して、監視対象の名前と、変化した際の処理を書く 第二引数は無くてもOK オプションを使用しない場合

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

オプションを使用する場合

プロパティ 説明
deep bool ネストされたオブジェクトも監視
immdiate bool 読み込み時に即呼び出し
new Vue({
  // ...
  watch: {
    list: {
      handler: function (newVal, oldVal) {
        // listが変化したときに行いたい処理
      },
      // ここからがオプション
      deep: true,
      immediate: true
    }
  }
})

インスタンスメソッドでの登録

this.$watchと書くと各メソッド内でウォッチャが使える

this.$watch('value', function(newVal, oldVal) {
  // ...
})

こんな風に書く,オプションの位置が独特

created: function () {
    this.$watch('value', function () {
        // ...処理をかく
    }, {
        immediate: true,
        deep: true
    })
}

ウォッチャの解除

インスタンスメソッドで登録した場合、返り値を使って監視を解除できる

var unwatch = this.$watch('value', handler)
unwatch()  // value の監視を解除

一度だけ動作するウォッチャ

unwatchを利用する

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
    })
  }
})

実行頻度の制御

フォーム入力などで監視を使うと、高頻度で値が変わるので、非同期通信が度々発生してパフォーマンスに影響する。その為setTimeoutLodashなどを利用してウォッチャの実行頻度を制御すると良い。

watch: {
    //  "_." なのでLodashですね 指定ミリ秒後にコールバック呼び出しをする
    value: _.debounce(function (newVal) {
        // ここへコストの高い処理を書く
        console.log(newVal)
      },
      // valueの変化が終わるのを待つ時間をミリ秒で指定
      500)
  }

デモ、 最後のミリ秒経過後に、フォーム入力後の値がconsoleに出力される

<input type="text" v-model="value">
new Vue({
    el: '#app',
    data: {
        value: '編集してみてね'
    },
    watch: {
        value: _.debounce(function (newVal) {
            // ここへコストの高い処理を書く
            console.log(newVal)
        },
            // valueの変化が終わるのを待つ時間をミリ秒で指定
            1500)
    }
})

複数の値を監視する

インスタンスメソッドを使い監視対象を関数にして登録する。

this.$watch(function () {
    return [this.width, this.height]
}, function(){  // 上で言ってる関数ってここの funtionの事?
    // width , height が変化した際の処理
})

解らん場合はリファレンス
Vue.js インスタンスメソッド / データ

vm.$watch( expOrFn, callback, [options] )

今回は時間がないけど、余裕があるならリファレンスを書籍を行ったり来たりすると理解は進みそう。

オプションに登録する場合は、算出プロパティを監視することができる

computed: {
    watchTarget: function () {
        return [this.width, this.height]
    }
},
// ここからオプション
watch: {
    // これで算出プロパティを監視できる
    watchTarget: function() { ... }
}

POINT

大事そうだけど省略

フォームを監視してAPIからデータを取得しよう

GitHub APIからリポジトリ一覧を取得するかんたんなアプリのデモ
axiosAJAX(非同期通信)のgetでAPIからの値を取得して画面に表示している。

<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>
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))
    }
  },
})

htmlのv-model="current"になんでもかんでも入れてる感じ?
開発コンソールを開きNetworkタブにあるgetパラメータをクリックすれば 取得した内容が見れる

要は以下の様なURLを生成している
https://api.github.com/search/repositories?q=topic:vue
https://api.github.com/search/repositories?q=topic:jQuery

Vue内のcurrent:では以下のGETのパラメータを生成している
q=topic:vue
q=topic:jQuery

で、帰ったJSONlistに入れて、html側でv-for="item in listのfor文で回して items内の配列の中に存在する項目full_nameをhtml側で表示している。

SECTION 18 フィルタでテキストの変換処理を行う

ここでいう「フィルタ」とは、文字数を丸める。数字にカンマを入れる。等のテキストベースの変換処理を指す。
注意: ローカルに登録した場合でもthisへのアクセスはできない

フィルタの使い方

{{ Mustache }} か、v-bindディレクティブに | で繋げて呼び出せる

<!-- Mustashのケース -->
{{ 対象のデータ | フィルタの名前 }}
<!-- v-bindのケース -->
<div v-bind:id="対象のデータ| フィルタの名前"></div>

ローカルへの登録

コンポーネントfiltersオプションに登録することで特定のコンポーネント内のみで仕様できる

new Vue({
  el: '#app',
  data: {
    price: 19800
  },
  filters: {
    localeNum: function (val) {
      return val.toLocaleString()
    }
  }
})
{{ price | localeNum }}円

こんな感じにつかえる
出力例 カンマ付き
19,800円

グローバルへの登録

グローバルメソッドVue.filterに登録すると全てのコンポーネントから利用できる

Vue.filter('localeNum' ,function(val){
    return val.toLocaleString()
})

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

引数を持たせることもできる

{{ message | filter(foo, 100) }}

この例は
第一引数messageプロパティの値
第二引数fooプロパティの値
第三引数100が受け取れる

複数のフィルタをつなげて使用する

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

こんな風に180度のラジアンを出して、それを四捨五入表記にできる

SECTION 19 力スタムディレクティブでデータを監視しながらDOMを操作する

v-bindの様なディレクティブを自作できる機能、DOMのAPIを使いたいケース等で、DOMの状態を検知しながらDOM操作ができる様になる。仮想DOMではないので描画の最適化はされない。

カスタムディレクティブの使い方

こんな風に任意の名前を付けられる?

<div v-hoge>  </div>

トリガとなるデータを値としてバインドすることもできる

<!-- valueが変化すれば呼び出される -->
<div v-hoge="value">  </div>

ローカルへの登録

カスタムディレクティブはdirectives:オプションに登録すると特定のコンポーネントで使える

new Vue({
  el: '#app',
  directives: {
    focus: {
      // 紐付いている要素がDOMに挿入されるとき
      inserted: function (el) {
        el.focus() // 要素にフォーカスを当てる(文字入力待ち状態)
      }
    }
  }
})
<input type="text"><!-- フォーカスされない -->
<input type="text" v-focus><!-- フォーカスされる -->

上のデモをすると入力フォームの二番目に |の点滅が入り、文字入力受付状態が確認できる。

グローバルへの登録

グローバルメソッドに登録する方法はVue.directiveメソッドを使う
コンポーネント以外でどこでも使う汎用的なものとかを登録すると良いかも?)

Vue.directive('forcus', {
    inserted: function (el) {
        el.focus() // 要素にフォーカスを当てる(文字入力待ち状態)
    }
})

使用可能なフック

メソッド名 タイミング
bind ディレクティブが初めて要素と紐づいた時に
inserted 紐づいた要素が親Nodeに挿入された時に
update (仮想DOM更新時)紐づいた要素を包含しているコンポーネントのVNodeが更新された時に
componentUpdated (仮想DOM更新時)包含しているコンポーネントと子コンポーネントのVNodeが更新された時に
unbind 紐づいていた要素からディレクティブが削除される時に
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')
  }
})

フックの引数

vnodeから呼び出したコンポーネントを参照できるが難しいらしいので割愛。
『公式ガイド、仮想DOMを読め』とある

引数 内容
el 以下省略、書籍参照のこと
binding
vnode
oldVnode

フックの関数による省略方法

第二引数に関数を入れると bindupdate にフックして同じ処理を呼び出せる。

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

動画の再生を操作する例

    <!-- 動画1 -->
    <video src="demo1.mp4" v-video="video1" width="400"></video>
    <button v-on:click="video1=true">再生</button>
    <button v-on:click="video1=false">停止</button>
    <br>
    <!-- 動画2 -->
    <video src="demo2.mp4" v-video="video2" width="400"></video>
    <button v-on:click="video2=true">再生</button>
    <button v-on:click="video2=false">停止</button>
new Vue({
    el: '#app',
    data: {
        video1: false,
        video2: false
    },
    directives: {
        video(el, binding) {
            binding.value ? el.play() : el.pause()
        }
    }
})

上記のコードをデモすると2つの動画をボタンで再生・停止できるようになる

基本的な読み方がいまだあいまいなので解説
html中のv-videoでバインドしたvideo1の値はまず,スクリプト内でfalseに設定されている video1: false,
再生ボタンを押すとvideo1の値がhtml側でtrueにされる

その値はスクリプト側のでdirectives:のhtmlと紐づけたvideo( ... )で監視されていて elで渡ったの引数に収納されたboolに対して、play(),またはpause()メソッドを実行している
という感じか?

ただし、この実装だと、video1のプロパティを変更すると、video2プロパティの要素のディレクティブも一緒に呼び出されるので、複雑な処理を書く際には不向きらしい。
次の方法はこれを改善できるという

前の状態と比較して処理を行う

力スタムディレクティブはメソッドの引数に古いVnodeと古いバインドデータを受け取れるので、バインドしたデータに変化があった際のみに処理を行わせることが可能。

arg 引数
modifiers 修飾子のオブジェクト
value 新しい値
odlvalue 古い値(updateとcomponetUpdateのみ)

では先ほどの動画デモを修正してoldValueプロパティの比較で無駄な処理を走らせないように改修したのが下の例

new Vue({
    el: '#app',
    data: {
        video1: false,
        video2: false
    },
    directives: {
        video(el, binding) {
            // ここに oldValue での判定を加えた
            if (binding.value !== binding.oldValue) {
                binding.value ? el.play() : el.pause()
            }
        }
    }
})

SECTION 20 nextTickで更新後のDOMにアクセスする

非同期処理が走ると、JSのコードの中では処理が遅れて、更新状態を判定できない事がままある。 (なので、非同期じゃないPHPなんかでPOSTとかすると、値が返るまでそこで処理が止まる。)
特にJSで非同期でDOM絡みの操作を書くと、非同期でPOST、GETをして値を取ったので、JS内で監視してても、それは先にプログラムが走り終わってるので、取得した値を元に処理ができない!
なんてことが発生してしまう。それを何とかしてくれるのがnextTickらしい
DOM更新を待ち受けて処理をしてくれるそうだ

nextTickの使い方

方法1, グローバルメソッドVue.nextTickのコールバック関数として定義する。
方法2, インスタンスメソッドとしてthis.$nextTickとして使う。

this.$nextTick(function(){
    // DOM更新後に行いたい処理を書く
})

これで、関数内の処理がDOM更新後に行うように予約できる

更新後のDOMの高さを取得しよう

例えば、プルダウンのリストの高さを通常のVue.js的な書き方で取得しようとした場合。 DOMへのアクセスをするなら$refsを使う

      console.log("通常:", this.$refs.list.offsetHeight);

CH2のおさらい ここから------
DOMにアクセスするには、インスタンスプロパティ$el,$refsを使用する。
但し、ライフサイクルフックのmounted以降でないと使えない

$refsの使い方

<div id="app">
    <p ref="hello">hello</p>
    <!--  p要素にhelloと名を付けた   -->
</div>

以下の様にアクセス

new Vue({
    el: '#app',
      mounted: function() {
      console.log(this.$refs.hello)  // p要素のDOMとなる
    }
})

$el や$refsは一時的な変更です!

これらは仮想DOMではないので描画処理の最適化をしない
操作の都度描画するので注意

CH2のおさらい ここまで------

でも、これだと処理の後にDOMが追加されてもその値を拾う事ができない。
こんなデモがある

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);
      });
    }
  }
});
<!-- list. の高さは最初は0なので数値1からを出力させる処理が,実はここに書いてある -->
<button v-on:click="list.push(list.length+1)">追加</button>
<ul ref="list">
    <li v-for="item in list">{{ item }}</li>
</ul>

開発タブでconsole見てボタンを押して<li>を追加しても 通常:の方の値は0となって取得できてない。が、$nextTickを使った方は上書きしたDOMからの値が取得できている

nextTickの注意点:
ウェブフォント、画像が含まれていた場合、それらのロード(詳細情報?)は持たないので、画像の高さは決め打ちにして計算して使う。といったテクニックが必要になる。ということらしいです。

まとめ

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