レンダリング関数と JSX


目次

  • 基本

  • ノード、ツリー、および仮想 DOM

  • ##createElement パラメーター

    • データ オブジェクトへのドリルダウン

    • 完全な例

    • #制約

  • テンプレート関数の代わりに JavaScript を使用する
    • v-if および v-for
    • ##v-model
    • イベントとキー修飾子
    • ##スロット


    • JSX
  • 機能コンポーネント

  • 属性の受け渡しおよびイベントから子要素または子コンポーネントへ

  • ##基本
  • Vue では、ほとんどの場合、テンプレートを使用して HTML を作成することをお勧めします。ただし、JavaScript の完全なプログラミング機能が本当に必要なシナリオもいくつかあります。現時点では、テンプレートよりもコンパイラに近い
  • レンダリング関数
を使用できます。


render 関数が非常に役立つ簡単な例を見てみましょう。いくつかのアンカー付きタイトルを生成したいとします。
<h1>
  <a name="hello-world" href="#hello-world">
    Hello world!
  </a>
</h1>
上記の HTML では、次のようにコンポーネント インターフェイスを定義することにします:
<anchored-heading :level="1">Hello world!</anchored-heading>

## のみを渡すことができるヘッダーの作成を開始するとき#level
prop が見出し (見出し) コンポーネントを動的に生成する場合、次のように実装することをすぐに思いつくかもしれません:

<script type="text/x-template" id="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>
</script>
Vue.component('anchored-heading', {
  template: '#anchored-heading-template',
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})
ここでテンプレートを使用することは最良の選択ではありません。コードが長くなるだけでなく、 <slot></slot>
と記述し、アンカー要素を挿入するときにもう一度繰り返します。

テンプレートはほとんどのコンポーネントで非常に便利ですが、ここでは明らかに適していません。そこで、render 関数を使用して上記の例を書き直してみましょう。

Vue.component('anchored-heading', {
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // 标签名称
      this.$slots.default // 子节点数组
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

見た目はずっとシンプルですね!この方法では、コードははるかに単純になりますが、Vue のインスタンス プロパティについてよく理解しておく必要があります。この例では、v-slot ディレクティブなしで子ノードをコンポーネントに渡すとき (anchored-HeadingHello world!## など) を知っておく必要があります。 、これらの子ノードはコンポーネント インスタンスの $slots.default に保存されます。まだ理解していない場合は、レンダリング関数に入る前に インスタンス プロパティ API を読むことをお勧めします。


#ノード、ツリー、仮想 DOM

レンダリング関数に入る前に、いくつかのことを学びましょう ブラウザがどのように動作するかは重要です。次の HTML 部分を例に挙げます。


<div>
  <h1>My title</h1>
  Some text content
  <!-- TODO: Add tagline -->
</div>

ブラウザがこのコードを読み取ると、すべてのコンテンツを追跡するために
"DOM ノード" ツリー

が構築されます。 like you 家族の成り立ちをたどる家系図を描くのと同じです。

上記の HTML に対応する DOM ノード ツリーは次のとおりです。

各要素はノードです。テキストの各部分もノードです。コメントもノードです。ノードはページの一部です。家系図と同様に、各ノードには子を持つことができます (つまり、各部分に他の部分を含めることができます)。 1.png

これらすべてのノードを効率的に更新するのは難しい場合がありますが、幸いなことに、手動で行う必要はありません。ページに表示したい HTML を Vue に伝えるだけです。これはテンプレート内で指定できます:

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

またはレンダリング関数内で指定できます:

render: function (createElement) {
  return createElement('h1', this.blogTitle)
}

どちらの場合も、Vue のページは次のようになります。

blogTitle

が変更された場合でも、自動的に更新され続けます。


仮想 DOM

Vue は、仮想 DOM

を作成することで自身を追跡します。実際の DOM を変更します。このコード行を注意深く見てください:

return createElement('h1', this.blogTitle)
createElement

正確には何が返されるのでしょうか?実際には実際の DOM 要素ではありません。より正確な名前は

createNodeDescription です。これに含まれる情報は、子ノードの説明情報など、ページ上にどの種類のノードをレンダリングする必要があるかを Vue に伝えるためです。このようなノードを「仮想ノード」と呼び、しばしば「VNode」と省略されます。 「仮想 DOM」とは、Vue コンポーネント ツリーから構築された VNode ツリー全体を指します。

createElement パラメータ


次に行う必要があることcreateElement 関数のテンプレートでこれらの関数を使用する方法をよく理解してください。 createElement が受け入れるパラメータは次のとおりです:

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一个 HTML 标签名、组件选项对象,或者
  // resolve 了上述任何一种的一个 async 函数。必填项。
  'div',
  // {Object}
  // 一个与模板中属性对应的数据对象。可选。
  {
    // (详情见下一节)
  },
  // {String | Array}
  // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)


データ オブジェクトの詳細

注意すべき点: v-bind:classv-bind:style がテンプレート構文で特別に扱われるのと同様に、これらには対応するものもあります。 VNode データ オブジェクトのトップレベル フィールド。このオブジェクトを使用すると、通常の HTML 属性だけでなく、innerHTML などの DOM 属性もバインドできます (これは v-html ディレクティブをオーバーライドします)。

{
  // 与 `v-bind:class` 的 API 相同,
  // 接受一个字符串、对象或字符串和对象组成的数组
  'class': {
    foo: true,
    bar: false
  },
  // 与 `v-bind:style` 的 API 相同,
  // 接受一个字符串、对象,或对象组成的数组
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 普通的 HTML 特性
  attrs: {
    id: 'foo'
  },
  // 组件 prop
  props: {
    myProp: 'bar'
  },
  // DOM 属性
  domProps: {
    innerHTML: 'baz'
  },
  // 事件监听器在 `on` 属性内,
  // 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
  // 需要在处理函数中手动检查 keyCode。
  on: {
    click: this.clickHandler
  },
  // 仅用于组件,用于监听原生事件,而不是组件内部使用
  // `vm.$emit` 触发的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
  // 赋值,因为 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',
  // 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
  // 那么 `$refs.myRef` 会变成一个数组。
  refInFor: true
}


完全な例

この知識があれば、当初達成したかったことを達成できるようになります。コンポーネント:

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) {
    // 创建 kebab-case 风格的 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: function (createElement) {
  var myParagraphVNode = createElement('p', 'hi')
  return createElement('div', [
    // 错误 - 重复的 VNode
    myParagraphVNode, myParagraphVNode
  ])
}

本当に要素/コンポーネントを何度も繰り返す必要がある場合は、ファクトリ関数を使用してそれを実現できます。たとえば、次のレンダリング関数は、完全に合法的な方法で 20 の同一の段落をレンダリングします。

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


テンプレート関数の代わりに JavaScript を使用する



##v-if および v -for#Vue のレンダリング関数は、ネイティブ JavaScript で簡単に実行できる操作に代わる独自の代替手段を提供しません。たとえば、テンプレートで使用されている

v-if

v-for:

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>
これらは、JavaScript の

if/else を使用してレンダリング関数で使用できます。

map を書き換えます:

props: ['items'],
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 との直接の対応関係はありません。対応するロジックを自分で実装する必要があります。

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

これは、最下層ですが、これにより、v-model よりもインタラクションの詳細をより詳細に制御できます。

#イベントとキー修飾子

.passive の場合、 .capture

.once はイベント修飾子です。Vue は、on: に使用できる対応するプレフィックスを提供します。

#修飾子プレフィックス##.passive~!##例: event.stopPropagation()
## ####&#################。捕獲############!########## ### ##.once~
##.capture.once または .once.capture
on: {
  '!click': this.doThisInCapturingMode,
  '~keyup': this.doThisOnce,
  '~!mouseover': this.doThisOnceInCapturingMode
}
他のすべての修飾子の場合、イベント ハンドラーでイベント メソッドを使用できるため、プライベート プレフィックスは必要ありません。
Modifier関数での同等の操作の処理
#.stop

##.prevent##event.preventDefault() #.selfif (event.target !== events.currentTarget) return, .13 (他のキー修飾子の場合は、).ctrl.shift, .meta は

すべての修飾子を使用した例を次に示します:

on: {
  keyup: function (event) {
    // 如果触发事件的元素不是事件绑定的元素
    // 则返回
    if (event.target !== event.currentTarget) return
    // 如果按下去的不是 enter 键或者
    // 没有同时按下 shift 键
    // 则返回
    if (!event.shiftKey || event.keyCode !== 13) return
    // 阻止 事件冒泡
    event.stopPropagation()
    // 阻止该元素默认的 keyup 事件
    event.preventDefault()
    // ...
  }
}


slot

you の内容静的スロットは this.$slots を通じてアクセスできます。各スロットは VNode 配列です:

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

this を通じてアクセスすることもできます。 $scopedSlots スコープ スロットにアクセスします。各スコープ スロットは、多数の VNode を返す関数です:

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

レンダリング関数を使用してエフェクトをサブコンポーネントのドメイン スロットに転送する場合は、次のことができます。 VNode データ オブジェクトの scopedSlots フィールドを使用します:

render: function (createElement) {
  return createElement('div', [
    createElement('child', {
      // 在数据对象中传递 `scopedSlots`
      // 格式为 { name: props => VNode | Array<VNode> }
      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: function (h) {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})

createElement のエイリアスとして h を使用することは、Vue エコシステムの一般的な規則であり、実際には JSX で必要です。 Vue の Babel プラグイン のバージョン 3.4.0 以降、ES2015 構文 (関数やアロー関数ではなく) で宣言された JSX を含むメソッドとゲッターに const h = を自動的に挿入します。 this.$createElement なので、(h) パラメータを削除できます。以前のバージョンのプラグインでは、h が現在のスコープで使用できない場合、アプリケーションはエラーをスローします。

JSX が JavaScript にどのようにマップされるかについて詳しくは、

使用法ドキュメントを参照してください。


機能コンポーネント


前に作成したアンカー タイトル コンポーネントは比較的単純です。状態を管理せず、渡された状態をリッスンせず、ライフサイクル メソッドもありません。実際、これはいくつかの小道具を受け入れる単なる関数です。


このようなシナリオでは、コンポーネントを

function としてマークできます。これは、コンポーネントがステートレス (応答データなし) でインスタンスなし (# なし) であることを意味します。 ##このコンテキスト)。

機能コンポーネント

は次のようになります:

Vue.component('my-component', {
  functional: true,
  // Props 是可选的
  props: {
    // ...
  },
  // 为了弥补缺少的实例
  // 提供第二个参数作为上下文
  render: function (createElement, context) {
    // ...
  }
})

注: 2.3.0 より前のバージョンでは、機能コンポーネントが props を受け取りたい場合は、props オプションが必要でした。バージョン 2.3.0 以降では、props オプションを省略でき、コンポーネントのすべてのプロパティが自動的かつ暗黙的に props に解決されます。

関数コンポーネントを使用する場合、関数コンポーネントはステートレスかつインスタンスレスであるため、参照は HTMLElement になります。

バージョン 2.5.0 以降では、単一ファイル コンポーネントを使用する場合、テンプレートベースの機能コンポーネントは次のように宣言できます:

<template functional>
</template>

コンポーネントに必要なものはすべて、context パラメーターを介して渡されます。このパラメーターは、次のフィールドを含むオブジェクトです:

  • props: 提供されるオブジェクトすべてのプロパティの

  • children: VNode 子ノードの配列

  • slots: 1 つの関数すべてのスロットを含むオブジェクトを返す

  • scopedSlots: (2.6.0) 渡されたスコープ付きスロットを公開するオブジェクト。通常のスロットも関数として公開します。

  • data: コンポーネントに渡される data オブジェクト 全体。createElement## の 2 番目のパラメータとしてコンポーネントに渡されます。

  • #parent: 親コンポーネントへの参照

  • listeners: (2.3.0 ) 現在のコンポーネントの親コンポーネントによって登録されたすべてのイベント リスナーを含むオブジェクト。これは data.on のエイリアスです。

  • injections: (2.3.0 ) inject オプションが使用される場合、このオブジェクトには、注入する必要があるプロパティが含まれています。

function: true を追加した後、アンカー タイトル コンポーネントのレンダリング関数を更新し、それに context パラメーターを追加する必要があります。 this.$slots.defaultcontext.children に更新し、this.levelcontext.props.level に更新します。

機能コンポーネントは単なる関数であるため、レンダリングのオーバーヘッドははるかに低くなります。

これらはラッパー コンポーネントとしても非常に便利です。たとえば、これを行う必要がある場合:

  • プログラムによって複数のコンポーネントのうち 1 つを選択して、代わりにレンダリングします。

  • 次のステップで

    childrenpropsdata は、子コンポーネントに渡す前に操作されます。

次は、受信プロパティの値に基づいてより具体的なコンポーネントをレンダリングできる

smart-list コンポーネントの例です:

var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }

Vue.component('smart-list', {
  functional: true,
  props: {
    items: {
      type: Array,
      required: true
    },
    isOrdered: Boolean
  },
  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
    )
  }
})


属性とイベントを子要素または子コンポーネントに渡す

通常のコンポーネントでは、プロパティとして定義されていない属性はコンポーネントのルート要素に自動的に追加され、既存の属性を同じ名前で置き換えるか、インテリジェントにそれらをマージします

ただし、機能コンポーネントでは、この動作を明示的に定義する必要があります。

Vue.component('my-functional-button', {
  functional: true,
  render: function (createElement, context) {
    // 完全透传任何特性、事件监听器、子节点等。
    return createElement('button', context.data, context.children)
  }
})

2 番目のパラメーターとして context.datacreateElement に渡すことにより、 my-function-button 上のすべてのプロパティとイベント リスナーを渡します。実際、これは非常に透過的であるため、これらのイベントには .native 修飾子さえ必要ありません。

テンプレートベースの機能コンポーネントを使用する場合は、属性とリスナーを手動で追加する必要もあります。独立したコンテキストにアクセスできるため、data.attrs を使用して任意の HTML 属性を渡すか、listeners (つまり、data.on エイリアス) を使用できます。任意のイベント リスナーを渡します。

<template functional>
  <button
    class="btn btn-primary"
    v-bind="data.attrs"
    v-on="listeners"
  >
    <slot/>
  </button>
</template>


slots() および children

なぜ slots()children の両方が必要なのか不思議に思うかもしれません。 slots().defaultchildren に似ていませんか?いくつかのシナリオではこれは当てはまりますが、それが次のような子ノードを持つ機能コンポーネントの場合はどうなるでしょうか?

<my-functional-component>
  <p v-slot:foo>
    first
  </p>
  <p>second</p>
</my-functional-component>

このコンポーネントの場合、children は 2 つの段落タグを提供しますが、slots().default は 2 番目の匿名段落タグ slots のみを渡します。 ().foo は、最初に指定された段落タグを渡します。 childrenslots() の両方があるため、コンポーネントに特定のスロット メカニズムを認識させるか、単に children を渡して他のコンポーネントに渡すかを選択できます。 に対処する必要があります。


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


Vue のテンプレートが実際にコンパイルされることを知りたいかもしれません。レンダリング機能。これは実装の詳細であり、通常は気にする必要はありません。ただし、テンプレートの機能がどのようにコンパイルされるかを知りたい場合は、非常に興味深いと思われるかもしれません。 Vue.compile を使用してテンプレート文字列をオンザフライでコンパイルする簡単な例を次に示します。


キーストローク: .enter
if (event.keyCode !== 13) return13 に変更できます)別のキーコード
修飾キー: .alt

if (!event.ctrlKey) return (置換 ctrlKey
altKey に変更されました, shiftKey または metaKey)