vue.js

Vue.js 2.0 の render について試してみた

More than 1 year has passed since last update.

はじめに :hatching_chick:

つい先日、Vue.js 2.0 が発表(日本語訳)されました:tada:。その発表の中で新しい機能として、renderタグ (render オプション)によって自分で view のレンダリングの制御できる機能があったので、早速試してみました。

注意事項 :warning:

発表されて pre-alpha なため、公式なドキュメントまだありません。今回この機能を試すにあたって、ソースコードを調べながらやっていますので、この記事はドキュメントが公開された際に、記事の説明と異なる可能性があります。また、今後の開発によっては、render 機能が変わる可能性があります。なので、今回の記事は、render というものが実際どういう風なものなのか、参考がてら読んでもらえればと思います。

hello world :hatched_chick:

render タグはどうやって使うのか? まずは、render タグを使った hello world 的なもので試しながらみましょう!

テンプレート:

<div id="app">
  <p>{{ hello }} <render method="render" :args="'world'"></render></p>
</div>

テンプレートは、単純にhello worldpタグ内で表示される、まさに 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 でレンダリング :baby_chick:

先ほどの 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 タグのレンダリング :chicken:

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) にあります。

コンポーネントのレンダリング :cooking:

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>

この例のテンプレートは、定義したコンポーネント component1component2 を動的に切り替える例のテンプレートです。
コンポーネントの切り替えは、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 オプションで定義したコンポーネントcomponent1component2 を同関数の引数 arg に渡ってきた値、dataオプションの component の値によって、コンポーネントの仮想 DOM を $createElement で生成しています。その際、コンポーネントで定義された props にデータを渡すには、$createElement の第2引数 dataprops を指定することで渡すことが可能です。コンポーネントに渡される値が変更されるかどうかは、inputタグのテキストを変更すれば確認できるはずなので、確認してみましょう!

この例では指定はありませんが、もちろん、第3引数 children に入れ子で <div>...</div>のようなものや、コンポーネントの仮想 DOM を指定することができます。

コンポーネント component2の定義している部分を見てもらえれば分かるかと思いますが、これまで、render によるレンダリングを説明してきましたが、render オプションに仮想 DOM を返すレンダリング関数を指定することも可能です。template オプションでテンプレートの指定がなく、render オプションにレンダリング関数の指定があれば、指定したレンダリング関数でレンダリングすることができます。

まとめ :rocket:

以上のまとめとして、render機能の要点をまとめます。

  • renderタグは method属性と args属性を指定する必要がある
  • method属性には、methodsオプションで定義した関数を指定する
  • args属性には呼び出すレンダリング関数に渡したい値をv-bindで指定する
  • renderタグから呼ばれるレンダリング関数は、仮想 DOM を返す必要がある
  • 仮想 DOM を生成するヘルパー的なものとして、$createElementインスタンスメソッドがあり、そのメソッドを利用してコンテンツを組み立てる
  • renderタグは子要素を持つことができ、それら子は、仮想 DOM としてレンダリング関数の第2引数に渡される
  • コンポーネントも$createElementを利用してレンダリングすることができる
  • renderオプションにも仮想 DOM を返す関数を指定することでレンダリングすることが可能

render について試してみた感想 :heart_eyes_cat:

  • slot のような子を動的にレンダリングするようなことや vue-router のような動的にコンポーネントを切り替えるものができそう
  • 動的なコンポーネントの定義・生成ができそう
  • Vue 2.0 の仮想 DOM の仕様を理解していないと辛い (JavaScript側でも宣言的に書きたいなあ。。。え、それJSXでできるって?)

とまあ、感想としてはこんな感じです。

最後に :exclamation:

この記事を書いて誤解されるかもしれないので、最後にコメントしておきます。

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 正式リリース時には意識することがなくなるかもしれません。