# Render 関数

Vue では、大多数のケースにおいてテンプレートを使ってアプリケーションを構築することを推奨していますが、完全な JavaScript プログラミングの力が必要になる状況もあります。そこでは私たちは render 関数 を使うことができます。

さあ、 render() 関数が実用的になる例に取りかかりましょう。例えば、アンカーつきの見出しを生成したいとします:

<h1>
  <a name="hello-world" href="#hello-world">
    Hello world!
  </a>
</h1>
1
2
3
4
5

アンカーつきの見出しはとても頻繁に使われますので、コンポーネントにするべきです:

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

コンポーネントは、level の値に応じた見出しを生成する必要があります。手っ取り早くこれで実現しましょう:

const app = Vue.createApp({})

app.component('anchored-heading', {
  template: `
    <h1 v-if="level === 1">
      <slot></slot>
    </h1>
    <h2 v-else-if="level === 2">
      <slot></slot>
    </h2>
    <h3 v-else-if="level === 3">
      <slot></slot>
    </h3>
    <h4 v-else-if="level === 4">
      <slot></slot>
    </h4>
    <h5 v-else-if="level === 5">
      <slot></slot>
    </h5>
    <h6 v-else-if="level === 6">
      <slot></slot>
    </h6>
  `,
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

このテンプレートは良いものには思えません。冗長なだけでなく、 <slot></slot> がすべての見出しのレベルにコピーされています。そして、アンカー要素を追加する時にはすべての v-if/v-else-if の分岐にまたコピーしなければなりません。

ほとんどのコンポーネントでテンプレートがうまく働くとはいえ、明らかにこれはそうではないものの一つです。そこで、 render() 関数を使ってこれを書き直してみましょう。

const app = Vue.createApp({})

app.component('anchored-heading', {
  render() {
    const { h } = Vue

    return h(
      'h' + this.level, // タグ名
      {}, // props/属性
      this.$slots.default() // 子供の配列
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

render() 関数の実装はとても単純ですが、コンポーネントインスタンスのプロパティについてよく理解している必要があります。この場合では、 anchored-heading の内側の Hello world! のように v-slot ディレクティブなしで子供を渡した時には、その子供は $slots.default() のコンポーネントインスタンスに保持されるということを知っている必要があります。もしまだ知らないのであれば、 render 関数に取り掛かる前に インスタンスプロパティ API を通読することをお勧めします。

# DOM ツリー

render 関数に取り掛かる前に、ブラウザがどのように動くのかについて少し知っておくことが重要です。この HTML を例にしましょう:

<div>
  <h1>My title</h1>
  Some text content
  <!-- TODO: Add tagline -->
</div>
1
2
3
4
5

ブラウザはこのコードを読み込むと、血縁関係を追跡するために家系図を構築するのと同じように、全てを追跡する 「DOM ノード」のツリー (opens new window)を構築します。

上の HTML の DOM ノードツリーはこんな感じになります。

DOM ツリーの可視化

すべての要素はノードです。テキストのすべてのピースはノードです。コメントですらノードです!それぞれのノードは子供を持つことができます。 (つまり、それぞれのノードは他のノードを含むことができます)

これらすべてのノードを効率的に更新することは難しくなり得ますが、ありがたいことに、それを手動で行う必要はありません。代わりに、テンプレートや render 関数で、ページ上にどのような HTML が欲しいかを Vue に伝えるのです。

テンプレート:

<h1>{{ blogTitle }}</h1>
1

または render 関数:

render() {
  return Vue.h('h1', {}, this.blogTitle)
}
1
2
3

そしてどちらの場合でも、 blogTitle が変更されたとしても Vue が自動的にページを最新の状態に保ちます。

# 仮想 DOM ツリー

Vue は、実際の DOM に反映する必要のある変更を追跡するために 仮想 DOM を構築して、ページを最新の状態に保ちます。この行をよく見てみましょう:

return Vue.h('h1', {}, this.blogTitle)
1

h() 関数が返すものはなんでしょうか?これは、 正確には 実際の DOM 要素ではありません。それが返すのは、ページ上にどんな種類のノードをレンダリングするのかを Vue に伝えるための情報をもったプレーンなオブジェクトです。この情報には子供のノードの記述も含まれます。私たちは、このノードの記述を 仮想ノード と呼び、通常 VNode と省略します。「仮想 DOM」というのは、Vue コンポーネントのツリーから構成される VNode のツリー全体のことなのです。

# h() の引数

h() 関数は VNode を作るためのユーティリティです。もっと正確に createVNode() と名づけられることもあるかもしれませんが、頻繁に使用されるので、簡潔さのために h() と呼ばれます。

// @returns {VNode}
h(
  // {String | Object | Function } tag
  // HTMLタグ名、コンポーネントまたは非同期コンポーネント
  // nullを返す関数を使用した場合、コメントがレンダリングされます。
  //
  // 必須
  'div',

  // {Object} props
  // テンプレート内で使うであろう属性、プロパティ、イベントに対応するオブジェクト
  //
  // 省略可能
  {},

  // {String | Array | Object} children
  // `h()` で作られた子供のVNode、または文字列(テキストVNodeになる)、
  // またはスロットをもつオブジェクト
  //
  // 省略可能
  [
    'Some text comes first.',
    h('h1', 'A headline'),
    h(MyComponent, {
      someProp: 'foobar'
    })
  ]
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 完全な例

この知識によって、書き始めたコンポーネントを今では完成させることができます:

const app = Vue.createApp({})

/** 子供のノードから再帰的にテキストを取得する */
function getChildrenTextContent(children) {
  return children
    .map(node => {
      return typeof node.children === 'string'
        ? node.children
        : Array.isArray(node.children)
        ? getChildrenTextContent(node.children)
        : ''
    })
    .join('')
}

app.component('anchored-heading', {
  render() {
    // 子供のテキストからケバブケース(kebab-case)のIDを作成する
    const headingId = getChildrenTextContent(this.$slots.default())
      .toLowerCase()
      .replace(/\W+/g, '-') // 英数字とアンダースコア以外の文字を-に置換する
      .replace(/(^-|-$)/g, '') // 頭と末尾の-を取り除く

    return Vue.h('h' + this.level, [
      Vue.h(
        'a',
        {
          name: headingId,
          href: '#' + headingId
        },
        this.$slots.default()
      )
    ])
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 制約

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

コンポーネント内のすべての VNode は一意でなければなりません。つまり、下のような render 関数は無効だということです:

render() {
  const myParagraphVNode = Vue.h('p', 'hi')
  return Vue.h('div', [
    // おっと - VNode が重複しています!
    myParagraphVNode, myParagraphVNode
  ])
}
1
2
3
4
5
6
7

もしあなたが本当に同じ要素、コンポーネントを何回もコピーしたいなら、ファクトリー関数を使えばできます。例えば、次の render 関数は 20 個の同じ段落をレンダリングする完全に正しい方法です。

render() {
  return Vue.h('div',
    Array.apply(null, { length: 20 }).map(() => {
      return Vue.h('p', 'hi')
    })
  )
}
1
2
3
4
5
6
7

# テンプレートの機能をプレーンな 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>
1
2
3
4

これは、render 関数では JavaScript の if/elsemap() で書き換えることができます。

props: ['items'],
render() {
  if (this.items.length) {
    return Vue.h('ul', this.items.map((item) => {
      return Vue.h('li', item.name)
    }))
  } else {
    return Vue.h('p', 'No items found.')
  }
}
1
2
3
4
5
6
7
8
9
10

# v-model

v-model ディレクティブは、テンプレートのコンパイル中に modelValueonUpdate:modelValue プロパティに展開されます - 私たちはそれらのプロパティを自分自身で提供する必要があります:

props: ['modelValue'],
render() {
  return Vue.h(SomeComponent, {
    modelValue: this.modelValue,
    'onUpdate:modelValue': value => this.$emit('update:modelValue', value)
  })
}
1
2
3
4
5
6
7

# v-on

私たちは適切なプロパティ名をイベントハンドラに与える必要があります。例えば、 click イベントをハンドルする場合は、プロパティ名は onClick になります。

render() {
  return Vue.h('div', {
    onClick: $event => console.log('clicked', $event.target)
  })
}
1
2
3
4
5

# イベント修飾子

.passive.capture.once イベント修飾子については、Vue はハンドラーのオブジェクトシンタックスを提供しています:

例えば:

render() {
  return Vue.h('input', {
    onClick: {
      handler: this.doThisInCapturingMode,
      capture: true
    },
    onKeyup: {
      handler: this.doThisOnce,
      once: true
    },
    onMouseover: {
      handler: this.doThisOnceInCapturingMode,
      once: true,
      capture: true
    },
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

その他すべてのイベントおよびキー修飾子については、特別な API は必要ありません。 なぜなら、ハンドラーの中でイベントのメソッドを使用することができるからです:

修飾子 ハンドラーでの同等の記述
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.currentTarget) return
Keys:
.enter, .13
if (event.keyCode !== 13) return (他のキー修飾子については、 13別のキーコード (opens new window) に変更する)
Modifiers Keys:
.ctrl, .alt, .shift, .meta
if (!event.ctrlKey) return (ctrlKey をそれぞれ altKeyshiftKeymetaKey に変更する)

これらすべての修飾子を一緒に使った例がこちらです:

render() {
  return Vue.h('input', {
    onKeyUp: event => {
      // イベントを発行した要素がイベントが紐づけられた要素ではない場合は
      // 中断する
      if (event.target !== event.currentTarget) return
      // 押されたキーが Enter(13) ではない場合、Shift キーが同時に押されて
      // いなかった場合は中断する
      if (!event.shiftKey || event.keyCode !== 13) return
      // イベントの伝播(propagation)を止める
      event.stopPropagation()
      // この要素のデフォルトの keyup ハンドラが実行されないようにする
      event.preventDefault()
      // ...
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# スロット

this.$slots から取得した VNode の配列でスロットの中身にアクセスすることができます:

render() {
  // `<div><slot></slot></div>`
  return Vue.h('div', {}, this.$slots.default())
}
1
2
3
4
props: ['message'],
render() {
  // `<div><slot :text="message"></slot></div>`
  return Vue.h('div', {}, this.$slots.default({
    text: this.message
  }))
}
1
2
3
4
5
6
7

render 関数で子コンポーネントにスロットを渡す方法:

render() {
  // `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
  return Vue.h('div', [
    Vue.h('child', {}, {
      // { name: props => VNode | Array<VNode> } の形で
      // 子供のオブジェクトを `slots` として渡す
      default: (props) => Vue.h('span', props.text)
    })
  ])
}
1
2
3
4
5
6
7
8
9
10

# JSX

たくさんの render 関数を書いていると、こういう感じのものを書くのがつらく感じるかもしれません:

Vue.h(
  'anchored-heading',
  {
    level: 1
  },
  [Vue.h('span', 'Hello'), ' world!']
)
1
2
3
4
5
6
7

特に、テンプレート版がそれにくらべて簡潔な場合は:

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

これが、Vue で JSX を使い、テンプレートに近い構文に戻す Babel プラグイン (opens new window) が存在する理由です。

import AnchoredHeading from './AnchoredHeading.vue'

const app = createApp({
  render() {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})

app.mount('#demo')
1
2
3
4
5
6
7
8
9
10
11
12
13

JSX がどのように JavaScript に変換されるのか、より詳細な情報は、 使用方法 (opens new window) を見てください。

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

あなたは Vue のテンプレートが実際に render 関数にコンパイルされることに興味があるかもしれません。これは通常知っておく必要のない実装の詳細ですが、もし特定のテンプレートの機能がどのようにコンパイルされるか知りたいのなら、これが面白いかもしれません。これは、 Vue.compile を使用してテンプレートの文字列をライブコンパイルする小さなデモです:

Deployed on Netlify.
最終更新日: 10/8/2020, 11:37:48 PM