Rendering-Funktionen und JSX


Inhaltsverzeichnis


Grundlagen


Vue In den meisten Fällen empfiehlt es sich, für die HTML-Erstellung Vorlagen zu verwenden. Es gibt jedoch einige Szenarien, in denen Sie wirklich die vollständigen Programmierfunktionen von JavaScript benötigen. Zu diesem Zeitpunkt können Sie die Rendering-Funktion verwenden, die näher am Compiler als an der Vorlage liegt.

Sehen wir uns ein einfaches Beispiel an, bei dem die render-Funktion sehr nützlich ist. Angenommen, wir möchten einige verankerte Titel generieren:

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

Für den obigen HTML-Code entscheiden Sie sich, die Komponentenschnittstelle wie folgt zu definieren:

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

Wenn Sie mit dem Schreiben eines Titels beginnen, der nur dynamisch generiert werden kann über die level prop-Überschrift Wenn Sie eine Komponente verwenden, denken Sie vielleicht schnell darüber nach, sie so zu implementieren:

<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>
rrree

Die Verwendung einer Vorlage ist hier nicht die beste Wahl: Der Code ist nicht nur langwierig, sondern auch <slot></slot> wird immer wieder in den Titel jedes Levels geschrieben . Dies wiederholt sich noch einmal beim Einfügen von Ankerelementen.

Während Vorlagen in den meisten Komponenten gut funktionieren, passen sie hier offensichtlich nicht. Versuchen wir also, das obige Beispiel mit der Funktion render umzuschreiben:

Vue.component('anchored-heading', {
  template: '#anchored-heading-template',
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

Sieht viel einfacher aus! Auf diese Weise ist der Code viel einfacher, Sie müssen jedoch mit den Instanzeigenschaften von Vue sehr vertraut sein. In diesem Beispiel müssen Sie wissen, dass, wenn Sie untergeordnete Knoten ohne eine v-slot-Anweisung an eine Komponente übergeben, z. B. anchored-heading innerhalb eines Hello world!, diese untergeordneten Knoten in $slots.default in der Komponenteninstanz gespeichert werden. Wenn Sie es noch nicht verstehen, empfehlen Sie, die Instance Properties API zu lesen, bevor Sie sich mit der Rendering-Funktion befassen.


Knoten, Bäume und virtuelles DOM


Bevor wir in die Rendering-Funktion eintauchen, lassen Sie uns Es ist wichtig, ein paar zu verstehen. Wie der Browser funktioniert. Nehmen Sie das folgende HTML-Stück als Beispiel:

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

Wenn der Browser diesen Code liest, erstellt er einen "DOM-Knoten"-Baum, um den Überblick über alles zu behalten, genau wie Sie zeichnen gerne einen Stammbaum, um die Entwicklung von Familienmitgliedern zu verfolgen.

Der dem obigen HTML entsprechende DOM-Knotenbaum ist wie folgt:

1.png

Jedes Element ist ein Knoten. Jeder Text ist auch ein Knoten. Sogar Kommentare sind Knoten. Ein Knoten ist ein Teil einer Seite. Genau wie bei einem Stammbaum kann jeder Knoten Kinder haben (d. h. jeder Teil kann andere Teile enthalten).

Es kann schwierig sein, alle diese Knoten effizient zu aktualisieren, aber zum Glück müssen Sie dies nicht manuell tun. Sie müssen Vue nur mitteilen, welchen HTML-Code Sie auf der Seite haben möchten. Dies kann in einer Vorlage erfolgen:

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

oder in einer Renderfunktion:

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

In beiden Fällen wird Vue die Seite erstellen automatisch aktualisiert, auch wenn sich blogTitle ändert.


Virtuelles DOM

Vue verfolgt sich selbst, indem es ein Virtuelles DOM erstellt. Wie um das echte DOM zu ändern. Bitte schauen Sie sich diese Codezeile genau an:

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

createElement Was genau wird zurückgegeben? Eigentlich kein tatsächliches DOM-Element. Der genauere Name lautet möglicherweise createNodeDescription, da die darin enthaltenen Informationen Vue mitteilen, welche Art von Knoten auf der Seite gerendert werden muss, einschließlich der Beschreibungsinformationen seiner untergeordneten Knoten. Wir beschreiben einen solchen Knoten als „virtuellen Knoten“ und kürzen ihn oft als „VNode“ ab. „Virtuelles DOM“ nennen wir den gesamten VNode-Baum, der aus dem Vue-Komponentenbaum erstellt wurde.


createElement Parameter


Als nächstes müssen Sie sich mit der Verwendung dieser Funktionen in der Vorlage im <🎜 vertraut machen > Funktion. Hier sind die von createElement akzeptierten Argumente: createElement

return createElement('h1', this.blogTitle)


Deep Data Object

Eines ist zu beachten: als

und v-bind:class Da sie in der Vorlagensyntax speziell behandelt werden, verfügen sie auch über entsprechende Felder der obersten Ebene im VNode-Datenobjekt. Mit diesem Objekt können Sie auch normale HTML-Attribute sowie DOM-Attribute wie v-bind:style binden (dies überschreibt die innerHTML-Direktive). v-html

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


Vollständiges Beispiel

Mit diesem Wissen können wir nun meine vervollständigen Die Komponente, die wir ursprünglich implementieren wollten:

{
  // 与 `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
}


Constraints

VNode Muss eindeutig sein

Alle VNodes im Komponentenbaum müssen eindeutig sein. Das bedeutet, dass die folgende Rendering-Funktion illegal ist:

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
    }
  }
})

Wenn Sie ein Element/eine Komponente wirklich viele Male wiederholen müssen, können Sie eine Factory-Funktion verwenden, um dies zu erreichen. Beispielsweise rendert die folgende Rendering-Funktion 20 identische Absätze auf völlig legale Weise:

render: function (createElement) {
  var myParagraphVNode = createElement('p', 'hi')
  return createElement('div', [
    // 错误 - 重复的 VNode
    myParagraphVNode, myParagraphVNode
  ])
}


unter Verwendung von JavaScript Ersetzen Sie die Vorlagenfunktion



v-ifv-ifv-for und

v-ifv-forDie Renderfunktionen von Vue bieten keine proprietären Alternativen zu irgendetwas, das einfach in nativem JavaScript erledigt werden kann. Beispielsweise werden

und

in Vorlagen verwendet: if/else

render: function (createElement) {
  return createElement('div',
    Array.apply(null, { length: 20 }).map(function () {
      return createElement('p', 'hi')
    })
  )
}
map Diese können in der Rendering-Funktion mithilfe von JavaScripts

und

verwendet werden Zum Umschreiben:

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

v-model

v-modelv-model

v-model

Rendering-Funktion Keine mit

Direkte Korrespondenz – Sie müssen die entsprechende Logik selbst implementieren:

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.')
  }
}

Dies ist der Preis dafür, tief in die unterste Ebene vorzudringen, aber im Vergleich zu

erhalten Sie dadurch eine bessere Kontrolle über die Interaktionsdetails.

.passive Ereignis- und Tastenmodifikatoren .capture.onceon

für <🎜>, <🎜> und <🎜> Für Für diese Ereignismodifikatoren stellt Vue entsprechende Präfixe bereit, die verwendet werden können <🎜>:<🎜>
修饰符前缀
.passive&
.capture!
.once~
.capture.once
.once.capture
~!

Zum Beispiel:

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)
      }
    }
  })
}

Für alle anderen Modifikatoren ist das private Präfix nicht notwendig, da Sie die Ereignismethode im Ereignishandler verwenden können:

修饰符处理函数中的等价操作
.stopevent.stopPropagation()
.preventevent.preventDefault()
.selfif (event.target !== event.currentTarget) return
按键:
.enter, .13
if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码)
修饰键:
.ctrl, .alt, .shift, .meta
if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKeyshiftKey 或者 metaKey)

Hier ist ein Beispiel mit allen Modifikatoren:

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


slots

you Du kannst bestehen this.$slots Greifen Sie auf den Inhalt statischer Slots zu, jeder Slot ist ein VNode-Array:

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

Sie können auch über this.$scopedSlots auf Scope-Slots zugreifen, jeder Scope-Slot ist eine Funktion, die mehrere VNodes zurückgibt:

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

Wenn Sie die Rendering-Funktion verwenden möchten, um den Scope-Slot an die untergeordnete Komponente zu übergeben, können Sie VNode verwenden scopedSlots Feld im Datenobjekt:

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


JSX


Wenn Sie A schreiben vielrender Funktion kann es schwierig sein, den folgenden Code zu schreiben:

render: function (createElement) {
  return createElement('div', [
    createElement('child', {
      // 在数据对象中传递 `scopedSlots`
      // 格式为 { name: props => VNode | Array<VNode> }
      scopedSlots: {
        default: function (props) {
          return createElement('span', props.text)
        }
      }
    })
  ])
}

Vor allem, wenn die entsprechende Vorlage so einfach ist:

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

Deshalb gibt es ein Babel Plugin wird verwendet, um die JSX-Syntax in Vue zu verwenden, wodurch wir zu einer Syntax zurückkehren können, die näher an Vorlagen liegt.

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

Die Verwendung von h als Alias ​​für createElement ist eine gängige Konvention im Vue-Ökosystem und wird tatsächlich von JSX benötigt. Ab der 3.4.0-Version des Babel-Plug-Ins von Vue werden wir const h = this.$createElement automatisch in jede Methode und jeden Getter einfügen, die JSX enthalten, die in der ES2015-Syntax deklariert sind (nicht in Funktionen oder Pfeilfunktionen), damit Sie Sie können den Parameter (h) entfernen. Bei früheren Versionen des Plugins gibt die Anwendung einen Fehler aus, wenn h im aktuellen Bereich nicht verfügbar ist.

Um mehr über die Zuordnung von JSX zu JavaScript zu erfahren, lesen Sie die Verwendungsdokumentation.


Funktionskomponente


Die zuvor erstellte Ankertitelkomponente ist relativ einfach verwaltet keinen Status, überwacht keinen an ihn übergebenen Status und verfügt über keine Lebenszyklusmethoden. Tatsächlich handelt es sich lediglich um eine Funktion, die einige Requisiten akzeptiert.

In einem solchen Szenario können wir die Komponente als functional markieren, was bedeutet, dass sie zustandslos ist (keine responsive Daten) und keine Instanzen hat (kein this-Kontext). ).

Eine Funktionskomponente sieht so aus:

import AnchoredHeading from './AnchoredHeading.vue'

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

Hinweis: In Versionen vor 2.3.0 war die Option props erforderlich, wenn eine Funktionskomponente Requisiten empfangen wollte. In Version 2.3.0 oder höher können Sie die Option props weglassen und alle Eigenschaften der Komponente werden automatisch und implizit in Requisiten aufgelöst.

Bei der Verwendung funktionaler Komponenten lautet die Referenz HTMLElement, da diese zustands- und instanzlos sind.

Wenn Sie in Version 2.5.0 und höher die Einzeldateikomponente verwenden, kann die vorlagenbasierte Funktionskomponente wie folgt deklariert werden:

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

Alles, was die Komponente benötigt, wird übergeben context Parameterübergabe, bei der es sich um ein Objekt mit den folgenden Feldern handelt:

  • props: ein Objekt, das alle Requisiten bereitstellt

  • children: VNode Array von untergeordneten Knoten

  • slots: Eine Funktion, die ein Objekt zurückgibt, das alle Slots enthält

  • scopedSlots: (2.6. 0 ) Ein Objekt, das den übergebenen Bereichsslot verfügbar macht. Stellt auch normale Slots als Funktionen bereit.

  • data: Das gesamte Datenobjekt , das als createElement an die Komponente übergeben wurde Der zweite an die Komponente übergebene Parameter

  • parent: ein Verweis auf die übergeordnete Komponente

  • listeners: (2.3.0 ) Ein Objekt, das alle von übergeordneten Komponenten für die aktuelle Komponente registrierten Ereignis-Listener enthält. Dies ist ein Alias ​​für data.on.

  • injections: (2.3.0 ) Dieses Objekt enthält, wenn die Option injectinject

    verwendet wird die Eigenschaften, die injiziert werden sollen.

functional: trueNach dem Hinzufügen von context müssen wir die Rendering-Funktion unserer Ankertitelkomponente aktualisieren, den Parameter this.$slots.default hinzufügen und context.children auf this.level aktualisieren. Aktualisieren Sie dann context.props.level auf

.

Da funktionale Komponenten nur Funktionen sind, ist der Rendering-Overhead viel geringer.

Sie sind auch als Wrapper-Komponenten sehr nützlich. Wenn Sie dies beispielsweise tun müssen:
  • Wählen Sie programmgesteuert eine von mehreren Komponenten aus, die in Ihrem Namen gerendert werden sollen
  • childrenIm 🎜> , props, data bearbeiten sie, bevor sie an untergeordnete Komponenten übergeben werden.

Das Folgende ist ein Beispiel für eine smart-list-Komponente, die Requisiten übergeben kann um eine spezifischere Komponente zu rendern:

<template functional>
</template>


Attribute und Ereignisse an untergeordnete Elemente oder untergeordnete Komponenten übergeben

In gewöhnlichen Komponenten werden Attribute, die nicht als Requisiten definiert sind, automatisch zum Stammelement der Komponente hinzugefügt und ersetzen vorhandene Attribute mit demselben Namen oder intelligenter Zusammenführung .

Für funktionale Komponenten müssen Sie dieses Verhalten jedoch explizit definieren:

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
    )
  }
})

Indem wir createElement als zweites Argument an context.data übergeben, geben wir my-functional-button alle oben genannten Eigenschaften und Event-Listener werden weitergegeben. Tatsächlich ist es so transparent, dass für diese Ereignisse nicht einmal der Modifikator .native erforderlich ist.

Wenn Sie vorlagenbasierte Funktionskomponenten verwenden, müssen Sie Attribute und Listener auch manuell hinzufügen. Da wir Zugriff auf den unabhängigen Kontext haben, können wir jedes HTML-Attribut mit data.attrs und jeden Ereignis-Listener mit listeners (ein Alias ​​für data.on) übergeben.

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


slots() vs. children

Sie fragen sich vielleicht, warum beides nötig istslots() und children. Ist slots().default nicht ähnlich zu children? In manchen Szenarien trifft das zu – aber was ist, wenn es sich um eine funktionale Komponente mit untergeordneten Knoten wie den folgenden handelt?

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

Für diese Komponente gibt Ihnen children zwei Absatz-Tags, während slots().default nur das zweite anonyme Absatz-Tag übergibt und slots().foo das erste benannte Absatz-Tag übergibt. Sie haben sowohl children als auch slots(), sodass Sie wählen können, ob die Komponente auf einen Slot-Mechanismus aufmerksam gemacht werden soll, oder children einfach an andere Komponenten übergeben werden soll, um damit umzugehen.


Vorlagenkompilierung


Es könnte Sie interessieren, dass die Vorlagen von Vue tatsächlich kompiliert wurden Rendering-Funktionen. Dies ist ein Implementierungsdetail und in der Regel kein Grund zur Sorge. Wenn Sie jedoch sehen möchten, wie die Funktionalität der Vorlage zusammengestellt ist, könnte es für Sie sehr interessant sein. Hier ist ein einfaches Beispiel für die Verwendung von Vue.compile zum spontanen Kompilieren einer Vorlagenzeichenfolge: