はじめに
こんにちは、とあるプロジェクトでアーキチームに所属している東郷です。
今回はVue.jsの最初の難関(だと思っている)「props down, event up」について、初心者(わたしが主に想定しているのは新卒入社の新人さんです)が読んでもわかるような資料を用意してみようと思います。
プロジェクトで独自のコンポーネントを作ったり、会社として用意しているコンポーネントの利用や改良ができるようになってもらいたいという思いから、その導入を解説しようということです。
ちなみに、私自身もアサインされて半年未満。Vue.jsを触り始めて4か月くらいです。
では、簡単なおさらいから始めていきます。
続編が公開されました。
Vue.jsとMVVM
props down, event upの理解のためにMVVMについて簡単に触れておきましょう。
下記は、Vueの公式サイト(https://012-jp.vuejs.org/guide/)から引用しました
Vue.js はインタラクティブな Web インターフェイスを作るためのライブラリです。
技術的に、Vue.js は MVVM パターンの ViewModel レイヤに注目しています。それは two way (双方向)バインディングによって View と Model を接続します。実際の DOM 操作と出力の形式はディレクティブとフィルタによって抽象化されています。
初心者にとってはMVVM パターンと言われてもピンとこないと思います。
そこで、導入として例を交えながら詳しく解説します。
MVVMはModel-View-ViewModelの頭文字
下の図はVue.jsの公式サイトから拝借しました。
図で見ると何となく分かった感じがしますが、何となくの理解ではなく確実に理解しましょう。
Vue.js 公式サイト(https://012-jp.vuejs.org/guide/)より
実体 | 役割 | |
---|---|---|
model | JavaScrptのコード | データ処理の主体 |
view | DOM(最終的なhtml) | 人間に情報を伝える、操作を受け付ける末端 |
ViewModel | vue.js | modelで処理したデータをどんなふうにveiwに流し込むかの制御、viewで受けた操作をmodelに伝える |
なぜ、アルファベットで表現してまで分割して考えるのか? それは、具体例を考えれば簡単に理解できるはずです。
ユーザに何らかのデータを伝えるとき、どんな見せ方をしますか? あるいはどんな見方をしたいですか? 文章、表、写真、動画、音楽の再生など、データに合わせて適切な見せ方が存在するはずです。では、適切な見せ方が決まって同じ種類のデータを扱うのなら、ある種のテンプレートにデータを流し込んで決まった見せ方にしますよね?
もし変えてしまったらユーザーは混乱しますし、そんな複雑なサイトを作るのは困難かつ望ましくありません。
上記の話の見せ方(ある種のテンプレート)の部分をview(見た目)としてDOMが担当します。viewに流しこむデータの取得・加工、viewで受けた操作の命令を受けるのはmodelとしてのJavaScriptです(写真管理のwebサービスなんかであればダウンロード操作など)
MVVMの実現のために重要なData binding
Vue.jsがMVVMを実現するために取り入れている仕組みにData bindingがあります。
Data bindingは、よく「データを流し込む目印を打ち込む」と表現されます。まさにこの言葉がすべてを表しています。本解説の肝、「props down, event up」で再度、上記の表現について触れます。Data Bindingは、その言葉が表すように __”データを特定の個所に結びつけます”__。
しがたって、元のデータが途中で変わっても目印を打ち込んであるので、自動で(Vue.jsが勝手に)目印を打ち込んだ箇所の値を書き換えてくれます。素敵ですね。
一方で誤解しやすいのがこのData bindingという考え方です。
ついつい、Vue.jsにおけるData bindingは、常に双方向にデータが流れ込むものだと思ってしまいます(特にv-modelに値をバインドすれば値の変更に対応できることを知ったばかりの初心者さんはそう思ってしまう)。もちろん、Vue.jsとしては双方向にデータのやり取りは可能です。しかし、単純なData bindingだけですべての仕様を実現することはできません。
次章では、実際のコードを見ながらData bindingがどんなふうに機能しているかを見ていきましょう。
※初心者の皆さんへ:
ちなみに、MVVMやData bindingはVue.js専用の言葉ではありません。
MVVMはプロダクトの構成パターン、Data bindingは仕組みの名前であり他の言語やFrameworkでも当然登場します。
props down, event up が何を意味するのか
ちょっと前置きが長くなりましたが、本題のprops down, event upについて、実例を交えながら解説をしていきます。
業務でVue.jsを使うとなると普通はVueCLIを用いた単一コンポーネントファイルによる開発になると思います。当社でもその形式を利用しています。
この記事の題材もそれに倣って、下記のようなファイルの構成で説明を進めます。
# フォルダの階層構造 |
※2020/04/14追記: なお今回の題材では、3つのコンポーネントを親子孫関係にしていますが、何階層にもわたってデータを連携するのは現場ではあまりお勧めされません。データとイベントの管理が大変になりますのでemitの乱用は避けるべきです。
親コンポーネントがもつデータを浅い階層でやり取りするため、再利用性の高いコンポーネントの利用/作成のためと思ってご覧ください。場合によってはVuexを使ったデータ管理も有効かもしれません
実際の画面はこんな感じです。
components
配下のvueファイルのソースを下記に示します。App.vue
は中身を空っぽにしてParentLayer.vue
を表示しているだけですので割愛します。
<template> |
model.testData
が3か所にbindされています。
1つ目は、templete
で直接使用するマスタッシュ構文で、
2つ目は、HTML5標準のinput
タグにv-model
ディレクティブで、
3つ目は、今回自作したchildren-layer
タグにv-model
ディレクティブで
使用しています。
1つ目のマスタッシュ構文は参照だけです。何も困りませんし、model.testData
が変更されれば勝手に変わります。
2つ目のinput
タグのv-model
ディレクティブでは、テキストボックスにmodel.testData
の値が勝手に入ってきますし、
上記の実装ならテキストボックスを編集すれば、model.testData
がバインドされた箇所すべてが変更された値に変わります。勝手に値が流れ込んできてくれるし、それを編集すれば他にもその変更が伝わります。つまり、__双方向に値が伝達されていっているように見えてしまいます__。
ここが、Vue.jsのありがたいところであり、props down, event upの理解を困難にする部分です。入力内容がmodel.testData
に自動反映される仕組みは次のChildLayer.vue
の説明と合わせて行います。
3つ目のchildren-layer
タグにv-model
ディレクティブで指定された値がどんなふうにChildLayer.vue
が受け取り、処理するかについてですが、ここからはコンポーネントの理解を深めつつ見ていく必要があります。では、ChildLayer.vue
のソースを見ながら確認します。
<template> |
ParentLayer.vue
でv-model="testData"
として流れ込んできた値は、ChildLayer.vue
でどんなふうに受け取り、処理しているのでしょうか?
答えは、ChildLayer.vue
のprops
のvalue
プロパティです。ParentLayer.vue
ではtestData
という変数で扱われていた値は、ChildLayer.vue
ではvalue
プロパティの値として扱われます。こうして、親コンポーネントから子コンポーネントへと値が流れ込んできます。ChildLayer.vue
内では、そのvalue
を4箇所で使っています。
1つ目は、マスタッシュ構文で、
2つ目は、input
の:value
で、
3つ目は、button
タグのクリックイベントの引数で、
4つ目は、さらに子コンポーネントのgrand-parent-layer
で使用しています。
ParentLayer.vue
との違いに気づきましたか?button
タグがあることが1番目立ちますがそれ以外です。
ParentLayer.vue
では、input
タグに対してv-model
を使ってバインドしていたのにChildLayer.vue
では、:value
にバインドしていて、@input
なんていうイベントも追加されています。なぜ、こんな違いがあるかというと、もう1つ見逃してはいけない違いがあるからです。
それは、バインドしている値がdata.model
に属している値か、コンポーネントのプロパティかということです。
プロパティはあくまで__読み取り専用__であり、それを直接書き換えることはできません。なぜ直接書き換えられないかというと、プロパティは、親コンポーネントが子コンポーネントに対して付与するものです。子コンポーネントから見た親コンポーネントは絶対的な存在で逆らうことは許されていません。子コンポーネントが自らのプロパティを勝手に変えるということは、親コンポーネントでの指定と不整合が起きることを意味します。そんなことができたら、混乱することは必至です。
では、ParentLayer.vue
やChildLayer.vue
のテキストボックスを変更したら、しっかりと変更が伝わったのは何故でしょうか?
答えは、@input
が重要な役割を果たしているからです。この@input
はそれが記載されているタグのinput
イベントが呼ばれるたびに実行され、そのたびにtest
というセンスのない名前のメソッドを実行します。
test (e) { |
上記のtest
というメソッドは何をしているかというと$emit
というメソッドを実行しています。'input'
というメソッドを引数e.target.value
で実行してほしいとお願いしているメソッドです。そう、勝手にプロパティを変更してはいけないので、変更する権限を持つ親コンポーネントに変更をお願いしているのです。
ここで、ParentLayer.vue
内のinput
タグでの双方向な値のやり取りを解説したいと思います。これは、親コンポーネントに対して、input
タグに指定されたv-model
は、実は、下記の実装と同じです。
<input id="ParentInput2" |
methods: { |
このinput
タグはプロパティではなく、普通のデータを扱っているわけですし、親コンポーネントへemitする必要はないだけで、
裏ではVue.jsが値が双方向に反映されているように見せているのです。
先のchild-layer
タグに戻ってしまいますが、こちらも
<!-- 自作の要素に対するデータバインディング --> |
methods: { |
と記載するのと同じことになります。流れを追っていくと、
子コンポーネントの
templete
部で__inputイベント__によって__メソッドA__を起動
script
部の__メソッドA__によって親コンポーネントへと__イベントB__をemit
親コンポーネント
templete
部のv-on(@)ディレクティブで__イベントB__を受けて__メソッドC__を起動
script
部の__メソッドC__によって親コンポーネントのデータの書き換え
ということをしています。
つまり、親から子へのデータの流れはData bindingによるデータの流し込み(props down:流れは高いところから低いところへ)、子から親へのデータの流れは$emit
によるイベントとメソッドのリレー(event up:上の立場の親が子のイベントを拾い上げる)ということで双方向バインディングを実現しています。
また、こういった複雑でわかりにくい複数の指定をひとまとめに指定できる構文を__糖衣構文__といいます。つまり、ParentLayer.vue
とChildLayer.vue
にあるinput
タグの指定はVue.jsから見たら同じなのです。
ここで、GrandChildLayer.vue
を見てみましょう。ソース内にもコメントで書いていますが、props
のvalue
を直接v-model
に放り込んでいるので、テキストボックスに入力をするたびにエラーが出ます。
<template> |
v-model
が:value
と@input
を1つにまとめて書いていると表現しましたが、value
プロパティじゃないほかの名前のプロパティへ値を渡したいこともあるでしょう。@input
でないイベントを拾いたいことことも考えられます。
Vue.jsとしてそういった要望に対応できるにmodelオプションというものを用意されています。必要に応じて勉強してみてください。またprops
や$emit
を使わない親子間データ連携もあります。特徴も違います。ぜひ使い分けてみててください。
まとめ
最後に言葉でしっかりと表現して自分のものにしておきましょう。
- Data bindingはあくまで、データの流れ込みの目印である
- 双方向に見えても、それは糖衣構文で暗黙的に変換がかかっているだけである。
- 親コンポーネントへのデータ連携は
$emit
を使って実装しないといけない。(親でもそれを拾い上げる実装が必要)