はじめに
つい先日、Vue.js 2.0 が発表(日本語訳)されました。その発表の中で新しい機能として、render
タグ (render
オプション)によって自分で view のレンダリングの制御できる機能があったので、早速試してみました。
注意事項
発表されて pre-alpha なため、公式なドキュメントまだありません。今回この機能を試すにあたって、ソースコードを調べながらやっていますので、この記事はドキュメントが公開された際に、記事の説明と異なる可能性があります。また、今後の開発によっては、render
機能が変わる可能性があります。なので、今回の記事は、render
というものが実際どういう風なものなのか、参考がてら読んでもらえればと思います。
hello world
render
タグはどうやって使うのか? まずは、render
タグを使った hello world 的なもので試しながらみましょう!
テンプレート:
<div id="app">
<p>{{ hello }} <render method="render" :args="'world'"></render></p>
</div>
テンプレートは、単純にhello world
とp
タグ内で表示される、まさに hello world 的なものです。hello
のデータバインディングによるテキスト展開と render
タグによって構成されています。
render
タグには、呼び出したいレンダリング関数を指定する method
属性 と その関数にパラメータとして渡したい args
属性を指定する必要があります。
method
属性に指定できるレンダリング関数は、methods
オプションで定義した関数です。
args
属性に指定できる値は、data
オプションで定義したデータや、props
オプションで定義したプロパティ、そして computed
オプションで定義した computed property を指定することができます。この属性に指定する注意点としては、現状 pre-alpha では、v-bind
(省略記法:
)ディレクティブで指定する必要があります。
このテンプレートの例では、method
属性は、methods
オプションで定義した render
関数、args
属性は文字列 world
(リテラル)を指定しています。
JavaScript 側の実装はこんな感じになります:
new Vue({
el: '#app',
data: {
hello: 'hello'
},
methods: {
render: function (arg) {
return arg
}
}
})
テンプレートで宣言した render
タグで呼び出される render
関数は、methods
オプションで引数を受け取る render
関数として定義します。render
タグによって呼び出されるレンダリング関数は、レンダリングする内容を返す必要があります。こうすることによって、Vue 2.0 がこの関数からレンダリングする内容を受け取ってレンダリングするという仕組みになっています。
上記のレンダリング関数の実装(render
関数)では、単純に引数で渡ってきた値をそのまま返しているので、この例では、結果的には render
タグの args
属性に指定した値、つまり world
という文字列がそのままレンダリングされます。
実際には、こんな感じでレンダリングされます:
<div id="app">
<p>hello world</p>
</div>
render
タグによるレンダリングどうでしたでしょうか?そんなに難しくないですよね?
なお、今回、render
機能を試すに当たって作成したGitHub のリポジトリを作成しました。上記の hello world な例を確認したい場合、git clone
してきてブラウザで hello.html
を開くと確認できるようになっていますので、動作が気になる方は確認してみてください。
仮想 DOM でレンダリング
先ほどの hello world の例では、単純に文字列をレンダリング関数で返してレンダリングしました。さて、<div>...</div>
のように入れ子になった複雑なコンテンツをレンダリングしたい場合はどうやるのでしょうか?
この場合、レンダリング関数で仮想 DOM を組み立てたものを返すことでレンダリングすることができます。
では、例を見てみましょう!まずは、テンプレートから:
<div id="app">
<render method="render" :args="message"></render>
</div>
テンプレートは、引数に message
を受け取って render
関数を呼び出すように、render
タグで宣言します。
JavaScript による実装は、以下のようにします:
new Vue({
el: '#app',
data: {
message: 'hello world'
},
methods: {
render: function (arg) {
return this.$createElement('div', { class: 'message' }, [ // <div>
this.$createElement('p', {}, [arg]) // <p>
])
}
}
})
レンダリング関数 render
の実装内容で、普段の Vue.js で見慣れない $createElement
という Vue インスタンスのメソッドがでてきました。この $createElement
はこのメソッドに**指定されたパラメータの情報を元に生成した仮想 DOM を返します。**仮想 DOM は Vue 2.0 ではタグ名、タグの属性値、タグの子要素、テキストノードなどを情報を格納したオブジェクトです。
現時点 pre-alphaで $createElement
に指定する引数の仕様は以下になります。
- 引数1
tag
: 生成する仮想 DOM のタグ名 - 引数2
data
: ハッシュで構成された属性値 (省略可能) - 引数3
children
: 生成する仮想 DOM に内包される子要素の配列 (省略可能) - 引数4
namespace
: 属する名前空間 (省略可能)
上記のレンダリング関数では、$createElement
と引数 children
を駆使して、p
タグを内包するdiv
タグを返す 仮想 DOM を生成するように実装されており、<div>...</div>
のような入れ子的なコンテンツをレンダリングする場合、こんな感じで仮想 DOM を組み立てて、それを返すことで、複雑なコンテンツをレンダリングすることが可能です。
なお、p
タグの仮想 DOM を生成する $createElement
では、引数 children
にはレンダリング関数のパラメータ arg
に渡ってきた値、つまり message
の文字列が指定していますが、テキストをテキストノードとしてレンダリングしたいの場合は、このように $createElement
を使わずとも直接文字列値を指定することでテキストノードの仮想 DOM を生成することができます。
この仮想 DOM でレンダリングする例は、以下のようにレンダリングされます。
<div id="app">
<div class="message">
<p>hello world</p>
</div>
</div>
この例は、GitHub のここ (vnode.html
) にあります。
子要素を持った render
タグのレンダリング
render
タグには、下記のようなタグ内部に子要素を持ったテンプレートもレンダリングすることが可能です。
テンプレート:
<div id="app">
<render method="render" :args="message">
<ul>
<li v-for="n in 5"></li>
</ul>
</render>
</div>
このテンプレートの render
タグは、v-for
ディレクティブで li
をレンダリングするようなものを含んでいますが、このような Vue.js のディレクティブを含んだ子要素を内包するものもレンダリングすることは可能です。この場合は、Vue によって評価されたものが仮想 DOM としてレンダリング関数の第2引数に渡されます。
これを踏まえて、下記のように第2引数で受け取った仮想 DOM の内容を元に同関数内で動的に子要素をゴニョゴニョとレンダリングするといったことも可能になります:
new Vue({
el: '#app',
data: {
message: 'hello world'
},
methods: {
render: function (arg, children) { // children は配列で渡ってくる
var ul = children[0] // <ul>
ul.children.forEach(function (li, index) {
li.text = arg + index
})
return children
}
}
})
この例は、以下のようにレンダリングされます。
<div id="app">
<ul>
<li>hello world0</li>
<li>hello world1</li>
<li>hello world2</li>
<li>hello world3</li>
<li>hello world4</li>
</ul>
</div>
この例は、GitHub のここ (child.html
) にあります。
コンポーネントのレンダリング
Vue.js はコンポーネントを多用します。コンポーネントをレンダリングするには、どうやるのでしょうか?
コンポーネントのレンダリングも、$createElement
を使用してレンダリングすることができます。
以下は、コンポーネントをレンダリングする例のテンプレート:
<div id="app">
message: <input type="text" v-model="message"></br>
<a href="javascript:void(0);" @click="component = 'component1'">component1</a>
<a href="javascript:void(0);" @click="component = 'component2'">component2</a>
<render method="render" :args="component"></render>
</div>
この例のテンプレートは、定義したコンポーネント component1
と component2
を動的に切り替える例のテンプレートです。
コンポーネントの切り替えは、render
タグと data
オプションの component
を使用して実現しています(コンポーネントの is
属性を利用すれば可能ですが、例のため、わざとこうしています)。
また、コンポーネントの props に親側から動的に値を受け渡すために、v-model
ディレクティブが宣言された input
タグを使用しています。
JavaScript 側の実装は、下記になります:
new Vue({
el: '#app',
data: {
component: 'component1',
message: 'hello world'
},
components: {
component1: {
props: ['message'],
data: function () {
return { show: true }
},
template: '<div>'
+ '<p v-if="show">component1: {{message}}</p>'
+ '</div>'
},
component2: {
props: ['message'],
data: function () {
return { show: true }
},
render: function () {
return this.$createElement('div', {}, [
this.$createElement('p', { directives: [{ name: 'show', value: this.show }] }, ['component2: ' + this.__toString__(this.message)])
])
}
}
},
methods: {
render: function (arg) {
return this.$createElement(arg, { props: { message: this.message } })
}
}
})
コンポーネントのレンダリングの例は、GitHub のここ (component.html
) にあります。
render
関数では、components
オプションで定義したコンポーネントcomponent1
と component2
を同関数の引数 arg
に渡ってきた値、data
オプションの component
の値によって、コンポーネントの仮想 DOM を $createElement
で生成しています。その際、コンポーネントで定義された props にデータを渡すには、$createElement
の第2引数 data
に props
を指定することで渡すことが可能です。コンポーネントに渡される値が変更されるかどうかは、input
タグのテキストを変更すれば確認できるはずなので、確認してみましょう!
この例では指定はありませんが、もちろん、第3引数 children
に入れ子で <div>...</div>
のようなものや、コンポーネントの仮想 DOM を指定することができます。
コンポーネント component2
の定義している部分を見てもらえれば分かるかと思いますが、これまで、render
によるレンダリングを説明してきましたが、render
オプションに仮想 DOM を返すレンダリング関数を指定することも可能です。template
オプションでテンプレートの指定がなく、render
オプションにレンダリング関数の指定があれば、指定したレンダリング関数でレンダリングすることができます。
まとめ
以上のまとめとして、render
機能の要点をまとめます。
-
render
タグはmethod
属性とargs
属性を指定する必要がある -
method
属性には、methods
オプションで定義した関数を指定する -
args
属性には呼び出すレンダリング関数に渡したい値をv-bind
で指定する -
render
タグから呼ばれるレンダリング関数は、仮想 DOM を返す必要がある - 仮想 DOM を生成するヘルパー的なものとして、
$createElement
インスタンスメソッドがあり、そのメソッドを利用してコンテンツを組み立てる -
render
タグは子要素を持つことができ、それら子は、仮想 DOM としてレンダリング関数の第2引数に渡される - コンポーネントも
$createElement
を利用してレンダリングすることができる -
render
オプションにも仮想 DOM を返す関数を指定することでレンダリングすることが可能
render
について試してみた感想
-
slot
のような子を動的にレンダリングするようなことや vue-router のような動的にコンポーネントを切り替えるものができそう - 動的なコンポーネントの定義・生成ができそう
- Vue 2.0 の仮想 DOM の仕様を理解していないと辛い (JavaScript側でも宣言的に書きたいなあ。。。え、それJSXでできるって?)
とまあ、感想としてはこんな感じです。
最後に
この記事を書いて誤解されるかもしれないので、最後にコメントしておきます。
Vue 2.0 でリリースされる予定の render
機能ですが、これは、テンプレートによる宣言ベースの view の開発の現状のスタイルを置き換えるものではありません。
Vue 2.0 の発表記事にも書いてありますが、これまでどおり、単一ファイルコンポーネント(Single File Components) や、template
オプションを利用した、テンプレートの宣言ベースによる開発を推奨しています。(そもそも、view の実装を全て JavaScript で開発するなんてやりたくないですよね?)
render
機能は、あくまでも、テンプレートの宣言ベースによるコンテンツレンダリングではできない複雑なことをしたい人のために機能として提供されているため、普通にアプリケーションを作る場合はあまり使わないでしょう。どちらかというとプラグインやサードベンダ向けのライブラリ作成者の人や、仮想 DOM 職人の人(いるのか?)にとってうれしい機能でしょう。
ただ、Vue 2.0 は、コンポーネントの実行環境であるランタイムと、テンプレートを仮想 DOM に変換するコンパイラが、モジュール化がされています。このため、事前にコンパイラでテンプレートを仮想 DOM にプリコンパイルし、それらとランタイムをプロダクション環境にアプリケーションをデプロイすることによって、レンダリングのパフォーマンスを最適化することも可能になっています。
その場合は、コンポーネントの例で少し取り上げましたが、現時点では、render
オプションにプリコンパイルされた関数を指定する必要がありますが、今後 vue-loader / vueify のようなツールが Vue 2.0 のコンパイラに対応することによって、将来 Vue 2.0 正式リリース時には意識することがなくなるかもしれません。