描画関数

基本

Vue ではほとんどの場合 HTML をビルドするためにテンプレートを使うことが推奨されます。しかし、JavaScript による完全なプログラミングパワーを必要とする状況もあります。そこでテンプレートへの代替で、よりコンパイラに近い 描画 (render) 関数が使用できます。

render 関数が実用的になりうる簡単な例を見てみましょう。では、アンカーヘッダを生成したいとします。

<h1>
<a name="hello-world" href="#hello-world">
Hello world!
</a>
</h1>

上記の HTML に対して、望ましいコンポーネントのインターフェイスを決めましょう。

<anchored-heading :level="1">Hello world!</anchored-heading>

level プロパティを元にしてヘッダを生成するだけのコンポーネントから始める時、すぐに以下の例にたどり着くでしょう。

<script type="text/x-template" id="anchored-heading-template">
<div>
<h1 v-if="level === 1">
<slot></slot>
</h1>
<h2 v-if="level === 2">
<slot></slot>
</h2>
<h3 v-if="level === 3">
<slot></slot>
</h3>
<h4 v-if="level === 4">
<slot></slot>
</h4>
<h5 v-if="level === 5">
<slot></slot>
</h5>
<h6 v-if="level === 6">
<slot></slot>
</h6>
</div>
</script>
Vue.component('anchored-heading', {
template: '#anchored-heading-template',
props: {
level: {
type: Number,
required: true
}
}
})

このテンプレートは良い気がしません。冗長なだけでなく、全てのヘッダレベルで <slot></slot> を重複させており、アンカー要素を追加したい時に同じことをする必要があります。コンポーネントは 1 つの root ノードのみ含めることができるので、全体もまた無駄な div で囲まれています。

テンプレートはほとんどのコンポーネントでうまく動作しますが、この例はそうではないことが明確です。では、 render 関数を使ってこれを書き直してみましょう。

Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // タグ名
this.$slots.default // 子の配列
)
},
props: {
level: {
type: Number,
required: true
}
}
})

少しシンプルになりましたね!コードは短くなりましたが、これもまた Vue インスタンスプロパティの豊富な知識を必要とします。このケースでは、 slot 属性無しでコンポーネントに子を渡した時に ( anchored-heading の内側の Hello world! のような) 、それらの子がコンポーネントインスタンス上の $slots.default にストアされているかを知っている必要があります。もしあなたがまだ知らないなら、render 関数に進む前に、 インスタンスプロパティAPI を読むことをオススメします。

createElement 引数

あなたが精通する必要がある 2 つ目のことは createElement 関数の中でテンプレートの機能をどのように使うかについてです。こちらが createElement の受け付ける引数です。

// @returns {VNode}
createElement(
// {String | Object | Function}
// HTML タグ名、コンポーネントオプションもしくは関数
// これらの一つを返します。必須です。
'div',
// {Object}
// テンプレート内で使うであろう属性と一致する
// データオブジェクト。任意です。
{
// (詳細は下の次のセクションをご参照ください)
},
// {String | Array}
// VNodes の子。任意です。
[
createElement('h1', 'hello world'),
createElement(MyComponent, {
props: {
someProp: 'foo'
}
}),
'bar'
]
)

データオブジェクト詳解

1 つ注意点: v-bind:classv-bind:style がテンプレート内で特殊な扱いをされているのと同じように、それらは VNode のデータオブジェクト内で自身のトップレベルフィールドを持ちます。

{
// `v-bind:class` と同じ API
'class': {
foo: true,
bar: false
},
// `v-bind:style` と同じ API
style: {
color: 'red',
fontSize: '14px'
},
// 通常の HTML 属性
attrs: {
id: 'foo'
},
// コンポーネントプロパティ
props: {
myProp: 'bar'
},
// DOM プロパティ
domProps: {
innerHTML: 'baz'
},
// v-on:keyup.enter などの修飾詞はサポートされませんが、
// "on" の配下にイベントハンドラはネストされます。
// その代わり、手動で keyCode をハンドラの中で
// 確認することが可能です。
on: {
click: this.clickHandler
},
// コンポーネントに限って、
// vm.$emit を使っているコンポーネントから emit されるイベントではなく
// ネイティブのイベントを listen することができます。
nativeOn: {
click: this.nativeClickHandler
},
// カスタムディレクティブ。バインディングの古い値は、
// Vue はあなたのバインディングを追跡するため、設定できません。
directives: [
{
name: 'my-custom-directive',
value: '2'
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// { name: props => VNode | Array<VNode> }
// の 形式でのスコープ付きスロット
scopedSlots: {
default: props => createElement('span', props.text)
},
// 他のコンポーネントの子があるならば、そのスロット名
slot: 'name-of-slot',
// 他の特殊なトップレベルのプロパティ
key: 'myKey',
ref: 'myRef'
}

完全な例

これらの知識を使えば、私たちが始めたコンポーネントを完了させることができるようになりました。

var getChildrenTextContent = function (children) {
return children.map(function (node) {
return node.children
? getChildrenTextContent(node.children)
: node.text
}).join('')
}
Vue.component('anchored-heading', {
render: function (createElement) {
// kebabCase id の作成
var headingId = getChildrenTextContent(this.$slots.default)
.toLowerCase()
.replace(/\W+/g, '-')
.replace(/(^\-|\-$)/g, '')
return createElement(
'h' + this.level,
[
createElement('a', {
attrs: {
name: headingId,
href: '#' + headingId
}
}, this.$slots.default)
]
)
},
props: {
level: {
type: Number,
required: true
}
}
})

制約

VNode は一意でなければならない

コンポーネントツリーの中で全ての VNode は一意でなければなりません。つまり、以下の render 関数は不正です。

render: function (createElement) {
var myParagraphVNode = createElement('p', 'hi')
return createElement('div', [
// うわ - 重複の VNodes !
myParagraphVNode, myParagraphVNode
])
}

もし同じ要素 / コンポーネントを何度も重複させたい場合には、factory 関数を使うことで対応できます。例えば、次の render 関数は 20 個の一意に特定できるパラグラフを描画する完全に正当な方法です。

render: function (createElement) {
return createElement('div',
Array.apply(null, { length: 20 }).map(function () {
return createElement('p', 'hi')
})
)
}

素の JavaScript を使ったテンプレート置換機能

v-ifv-for

どんなところでも素の JavaScript で簡単に物事は成し遂げることができるので、Vue の render 関数は独自の代替手段を提供しません。例えば、v-ifv-for を使っているテンプレート内です。

<ul v-if="items.length">
<li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>

これは render 関数の中で JavaScript の if / elsemap を使って書き換えることができます。

render: function (createElement) {
if (this.items.length) {
return createElement('ul', this.items.map(function (item) {
return createElement('li', item.name)
}))
} else {
return createElement('p', 'No items found.')
}
}

v-model

描画関数には直接的な v-model の対応はありません。 あなた自身でロジックを実装する必要があります:

render: function (createElement) {
var self = this
return createElement('input', {
domProps: {
value: self.value
},
on: {
input: function (event) {
self.value = event.target.value
}
}
})
}

これは低レベルのコストですが、v-model と比較してインタラクションをより詳細に制御することもできます。

イベントとキー修飾子

.capture.once イベント修飾子に対して、Vue は on で使用できる接頭辞を提供しています:

修飾子 接頭辞
.capture !
.once ~
.capture.once または
.once.capture
~!

例:

on: {
'!click': this.doThisInCapturingMode,
'~keyup': this.doThisOnce,
'~!mouseover': this.doThisOnceInCapturingMode
}

全ての他のイベントとキー修飾子に対しては、単にハンドラ内のイベントメソッドを使用できるため、独自の接頭辞は必要ありません:

修飾子 ハンドラに相当
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.currentTarget) return
キー:
.enter, .13
if (event.keyCode !== 13) return (他のキー修飾子に対して、13別のキーコードに変更する)
修飾子キー:
.ctrl, .alt, .shift, .meta
if (!event.ctrlKey) return (ctrlKeyaltKeyshiftKey、または metaKey、それぞれ変更する)

以下に、これらの修飾子がすべて一緒に使用されている例を示します:

on: {
keyup: function (event) {
// イベントを発行している要素が、
// イベントが束縛されている要素でない場合は中止します。
if (event.target !== event.currentTarget) return
// up したキーが enter キー (13) でなく、
// shift キーが同時に押されていない場合は中止します。
if (!event.shiftKey || event.keyCode !== 13) return
// イベントの伝播を停止します。
event.stopPropagation()
// この要素に対する、デフォルトの keyup ハンドラを無効にします。
event.preventDefault()
// ...
}
}

スロット

this.$slots から VNode の配列として静的なスロットの内容にアクセスできます:

render: function (createElement) {
// <div><slot></slot></div>
return createElement('div', this.$slots.default)
}

そして、this.$scopedSlots から VNode を返す関数としてスコープ付きスロットにアクセスできます:

render: function (createElement) {
// <div><slot :text="msg"></slot></div>
return createElement('div', [
this.$scopedSlots.default({
text: this.msg
})
])
}

スコープ付きスロットを描画関数を使って子コンポーネントに渡すには、VNode データの scopedSlots フィールドを使います:

render (createElement) {
return createElement('div', [
createElement('child', {
// { name: props => VNode | Array<VNode> } の形式で
// scopedSlots を データオブジェクトに渡す
scopedSlots: {
default: function (props) {
return createElement('span', props.text)
}
}
})
])
}

JSX

もしあなたが多くの render 関数を書いている場合は、このような書き方の方が良いと感じるかもしれません。

createElement(
'anchored-heading', {
props: {
level: 1
}
}, [
createElement('span', 'Hello'),
' world!'
]
)

特にテンプレートが簡単なバージョンの場合は、次のようになります:

<anchored-heading :level="1">
<span>Hello</span> world!
</anchored-heading>

そのような理由から Vue で JSX を使うための Babel プラグイン があります。よりテンプレートに近い文法が戻ってきました。

import AnchoredHeading from './AnchoredHeading.vue'
new Vue({
el: '#demo',
render (h) {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})

createElementh にエイリアスしていることは、 Vue のエコシステムの中でよく見かける慣習です。そして、それは実は JSX には必須です。もし h がそのスコープ内で利用可能でない場合、アプリケーションはエラーを throw するでしょう。

より詳しい JSX の JavaScript へのマップの仕方については、usage ドキュメント をご参照ください。

関数型コンポーネント

先ほど作成したアンカーヘッダコンポーネントは比較的シンプルです。状態の管理や渡された状態の watch をしておらず、また、何もライフサイクルメソッドを持ちません。実際、これはいくつかのプロパティを持つただの関数です。

このようなケースにおいて、私たちは 関数型 としてのコンポーネントと特徴づけることができます。それは状態を持たない ( data が無い) でインスタンスを持たない ( this のコンテキストが無い) ことを意味します。関数型コンポーネント は次のような形式をしています。

Vue.component('my-component', {
functional: true,
// インスタンスが無いことを補うために、
// 2 つ目の context 引数が提供されます。
render: function (createElement, context) {
// ...
},
// プロパティは任意です
props: {
// ...
}
})

このコンポーネントが必要な全てのことは context を受け取ることです。それは次を含むオブジェクトです。

functional: true を追加した後、私たちのアンカーヘッダコンポーネントの render 関数の更新として単に必要になるのは、
context 引数の追加、this.$slots.defaultcontext.children への更新、this.levelcontext.props.level への更新でしょう。

関数型コンポーネントはただの関数なので、描画コストは少ないです。また、ラッパーコンポーネントとしてもとても便利です。例えば、以下が必要な時に。

こちらが、渡されるプロパティに応じてより具体的なコンポーネントに委譲する smart-list コンポーネントの例です。

var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }
Vue.component('smart-list', {
functional: true,
render: function (createElement, context) {
function appropriateListComponent () {
var items = context.props.items
if (items.length === 0) return EmptyList
if (typeof items[0] === 'object') return TableList
if (context.props.isOrdered) return OrderedList
return UnorderedList
}
return createElement(
appropriateListComponent(),
context.data,
context.children
)
},
props: {
items: {
type: Array,
required: true
},
isOrdered: Boolean
}
})

slots() vs children

もしかするとなぜ slots()children の両方が必要なのか不思議に思うかもしれません。 slots().defaultchildren と同じではないのですか?いくつかのケースではそうですが、もし以下の子を持つ関数型コンポーネントの場合はどうなるでしょう。

<my-functional-component>
<p slot="foo">
first
</p>
<p>second</p>
</my-functional-component>

このコンポーネントの場合、 children は両方のパラグラフが与えられます。slots().default は 2 つ目のものだけ与えられます。そして slots().foo は 1 つ目のものだけです。したがって、 childrenslots() の両方を持つことで このコンポーネントが slot システムを知っているか、もしくは、単純に children を渡しておそらく他のコンポーネントへその責任を delegate するかどうかを選べるようになります。

テンプレートのコンパイル

もしかすると Vue のテンプレートが実際に render 関数にコンパイルされることを知ることに興味を持つかもしれません。これは普段あなたは知る必要もない実装の詳細ですが、どのように特定のテンプレート機能がコンパイルされるかをもし見たいならこれに興味を持つかもしれません。以下は Vue.compile を使ってテンプレート文字列をその場でコンパイルをするちょっとしたデモです。

{{ result.render }}
_m({{ index }}): {{ fn }}
{{ result.staticRenderFns }}
{{ result }}