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

- 作者: mio
- 出版社/メーカー: シーアンドアール研究所
- 発売日: 2018/05/29
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
さすがに輪読担当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)がしていること
get
でwidth
の値を取得して
set
でhalfWidth
の値に代入する
つまり値を順繰りに回しているループしないか不安になるが、これで値をインタラクティブで双方向に連携ができる
算出プロパティのキャッシュ機能
算出プロパティとメソッド(関数)の違い
算出プロパティ リアクティブなデータをキャッシュ化してしまう。
<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 }) } })
実行頻度の制御
フォーム入力などで監視を使うと、高頻度で値が変わるので、非同期通信が度々発生してパフォーマンスに影響する。その為setTimeout
やLodash
などを利用してウォッチャの実行頻度を制御すると良い。
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からリポジトリ一覧を取得するかんたんなアプリのデモ
axios
のAJAX(非同期通信)の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
で、帰ったJSONをlist
に入れて、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 |
フックの関数による省略方法
第二引数に関数を入れると bind
と update
にフックして同じ処理を呼び出せる。
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