Функции рендеринга и JSX
основы
Для создания нашего HTML-кода vue рекомендует использовать шаблоны в большинстве случаев. Однако существуют ситуации, когда нам действительно нужна полная программная мощь JavaScript. Именно в таких случаях мы используем функцию рендеринга, которая ближе к альтернативе компилятора по сравнению с шаблонами.
Простой пример, который демонстрирует, где была бы полезна функция рендера, это когда мы хотим сгенерировать привязанные заголовки:
<h1>
<a name="hello-mumbai" href="#hello-world">
Hello Mumbai!
</a>
</h1>
Для приведенного выше фрагмента HTML мы можем решить, что нам нужен интерфейс компонента ниже:
<anchored-heading :level="1">Hello Mumbai!</anchored-heading>
Когда мы начнем с компонента, который генерирует заголовок только на основании уровня, мы быстро придем к следующему:
HTML
<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>
JS
Vue.component('anchored-heading', {
template: '#anchored-heading-template',
props: {
level: {
type: Number,
required: true
}
}
})
Приведенный выше шаблон выглядит не очень хорошо, он многословен, и мы дублируем <slot> </ slot> для каждого уровня заголовка, и мы должны будем сделать то же самое, когда добавим элемент привязки.
Хотя шаблоны будут отлично работать для большинства компонентов, это явно не один из этих компонентов. Давайте попробуем переписать его с помощью функции рендеринга:
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // tag name
this.$slots.default // array of children
)
},
props: {
level: {
type: Number,
required: true
}
}
})
Это намного проще и код короче, но также требует большего знакомства со свойствами экземпляра Vue. В этом случае вы должны знать, когда передавать дочерние элементы без директивы v-slot в компонент. Как Привет Мумбаи! Внутри привязанного заголовка дочерние элементы хранятся в экземпляре компонента в $ slots.default.
Noder, деревья и виртуальный DOM
Прежде чем углубляться в функции рендеринга, важно немного узнать о том, как работают браузеры. Рассмотрим этот HTML, например:
<div>
<h1>My title</h1>
Some text content
<!-- TODO: Add tagline -->
</div>
Когда браузер читает этот код, он создает дерево «узлов DOM», которое поможет ему отслеживать все, так же, как мы можем построить семейное дерево, чтобы отслеживать нашу расширенную семью.
Каждый элемент и фрагмент текста является узлом. Комментарии также являются узлами. Узел - это любая часть на странице. Как и в семейном древе, каждый узел может иметь детей.
Эффективное обновление этих узлов может быть трудным, и хорошо, нам никогда не придется делать это вручную. Скорее, мы сообщаем Vue, какой HTML мы хотим на странице.
В шаблоне:
<h1>{{ blogTitle }}</h1>
Или в функции рендеринга:
render: function (createElement) {
return createElement('h1', this.blogTitle)
}
В обоих случаях Vue будет автоматически обновлять страницу, даже когда изменяется blogTitle.
Виртуальный ДОМ
Vue реализует свою реактивную природу, создавая виртуальный DOM, чтобы отслеживать изменения, которые необходимо внести в настоящий DOM. Рассмотрим эту строку в функции рендеринга:
return createElement('h1', this.blogTitle)
createElement на самом деле не возвращает реальный элемент DOM. он может быть более точно назван createNodeDescription, потому что он содержит информацию, описывающую Vue, какой тип узла он должен отображать на странице. Включая описание любого дочернего узла. Это описание узла называется «виртуальным узлом» и обычно сокращается до VNode. «Виртуальный DOM» - это то, что мы называем всем деревом VNodes, построенным из дерева Vue Components.
Аргументы createElement
Следующее, с чем мы должны познакомиться, это то, как использовать функции шаблона в функции createElement.
// @returns {VNode}
createElement(
// {String | Object | Function}
// component options, HTML tag name, or async
// function that resolves to one of these. Required.
'div',
// {Object}
//this is a data object corresponding to the attributes
// that you would use in a template. Optional.
{
// (please see details in the next section below)
},
// {String | Array}
// the Children VNodes, these are built using `createElement()`,
// or by using strings to get 'text VNodes'. Optional.
[
'Some text comes first.',
createElement('h1', 'A headline'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
Глубина объекта данных
Следует отметить одну вещь: подобно тому, как v-bind: style и v-bind: class имеют специальную обработку в шаблонах, они также имеют свои собственные поля верхнего уровня в объектах данных VNode. Этот объект также позволяет нам связывать обычные атрибуты HTML, а также свойства DOM, такие как innerHTML (это заменит директиву v-html):
{
// Same API as `v-bind:class`, accepting either
// a string, object, or array of strings and objects.
class: {
foo: true,
bar: false
},
// Same API as `v-bind:style`, accepting either
// a string, object, or array of objects.
style: {
color: 'red',
fontSize: '14px'
},
// Normal HTML attributes
attrs: {
id: 'foo'
},
// Component props
props: {
myProp: 'bar'
},
// DOM properties
domProps: {
innerHTML: 'baz'
},
// Event handlers are nested under `on`, though
// modifiers such as in `v-on:keyup.enter` are not
// supported. You'll have to manually check the
// keyCode in the handler instead.
on: {
click: this.clickHandler
},
// For components only. Allows you to listen to
// native events, rather than events emitted from
// the component using `vm.$emit`.
nativeOn: {
click: this.nativeClickHandler
},
// Custom directives. Note that the `binding`'s
// `oldValue` cannot be set, as Vue keeps track
// of it for you.
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// Scoped slots in the form of
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// The name of the slot, if this component is the
// child of another component
slot: 'name-of-slot',
// Other special top-level properties
key: 'myKey',
ref: 'myRef',
// If you are applying the same ref name to multiple
// elements in the render function. This will make `$refs.myRef` become an
// array
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) {
// create 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 должен быть уникальным
Все VN-узлы в дереве компонентов должны быть уникальными, поэтому приведенный ниже код недействителен:
render: function (createElement) {
var myParagraphVNode = createElement('p', 'hi')
return createElement('div', [
// Yikes - duplicate VNodes!
myParagraphVNode, myParagraphVNode
])
}
Чтобы дублировать один и тот же элемент / компонент много раз, мы можем использовать фабричную функцию. Пример:
render: function (createElement) {
return createElement('div',
Array.apply(null, { length: 20 }).map(function () {
return createElement('p', 'hi')
})
)
}
Замена функций шаблона простым JavaScript
v-if и v-for
Функции рендеринга не предоставляют проприетарной альтернативы везде, где что-либо может быть достигнуто простым 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>
Мы можем переписать это if / else JavaScript и отобразить в функции рендеринга
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-модель
v-модель не имеет прямого аналога в функциях рендеринга - мы должны сами реализовать логику:
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)
}
}
})
}
Модификаторы событий и ключей
Для модификаторов событий .once, .passive и .capture Vue предлагает нам префиксы, которые можно использовать с on:
Модификатор (ы) | Префикс |
---|---|
.passive | & |
.захватить | ! |
.один раз | ~ |
.once.capture или .capture.once | ~! |
Пример:
on: {
'!click': this.doThisInCapturingMode,
'~keyup': this.doThisOnce,
'~!mouseover': this.doThisOnceInCapturingMode
}
Для всех других модификаторов событий и ключей, кроме этих, проприетарный префикс не нужен, потому что мы можем использовать метод события в обработчике.
игровые автоматы
Мы можем получить доступ к содержимому статических слотов как массивы VNodes из этого. $ Slots:
render: function (createElement) {
// `<div><slot></slot></div>`
return createElement('div', this.$slots.default)
}
Мы можем получить доступ к слотам с областями видимости как функциям, которые возвращают VNodes из этого. $ ScopedSlots:
`props: ['message'],
render: function (createElement) {
// `<div><slot :text="message"></slot></div>`
return createElement('div', [
this.$scopedSlots.default({
text: this.message
})
])
}
Чтобы передать слоты с ограниченным объемом в дочерний компонент с помощью функций рендеринга, мы используем поле scopedSlots в данных VNode:
render: function (createElement) {
return createElement('div', [
createElement('child', {
// pass `scopedSlots` in the data object
// in the form of { name: props => VNode | Array<VNode> }
scopedSlots: {
default: function (props) {
return createElement('span', props.text)
}
}
})
])
}
JSX
Если мы пишем много функций рендеринга, может быть больно писать что-то вроде этого:
createElement(
'anchored-heading', {
props: {
level: 1
}
},
[
createElement('span', 'Hello'),
' world!'
]
)
Особенно, когда версия шаблона настолько проста в сравнении:
<anchored-heading :level="1">
<span>Hello</span> world!
</anchored-heading>
Вот почему есть плагин Babel для использования JSX с Vue, что возвращает нас к синтаксису, который ближе к шаблонам:
import AnchoredHeading from './AnchoredHeading.vue'
new Vue({
el: '#demo',
render: function (h) {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
Функциональные компоненты
Привязанный компонент заголовка, который мы создали ранее, относительно прост. Он не управляет каким-либо переданным ему состоянием и не имеет методов жизненного цикла. Это только функция, которая имеет некоторые реквизиты.
Когда компонент помечается как функциональный, это означает, что он не имеет состояния (без реактивных данных) и экземпляров (без этого контекста). Функциональный компонент будет выглядеть так:
Vue.component('my-component', {
functional: true,
// the Props are optional
props: {
// ...
},
// in compensation for the lack of an instance,
// we are provided a 2nd context argument.
render: function (createElement, context) {
// ...
}
})
Мы передаем все, что нужно компоненту через контекст, это объект, содержащий:
- реквизит: это объект предоставленного реквизита
- дети: это массив детей VNode
- slots: это функция, возвращающая объект slots
- scopedSlots: from from (2.6.0+) Объект, который предоставляет переданные области действия. Это также выставляет нормальные слоты как функции.
- данные: это весь объект данных, который передается компоненту как второй аргумент createElement
- parent: это ссылка на родительский компонент
- listeners: from (2.3.0+) Объект, содержащий зарегистрированных слушателей событий. Какой псевдоним для data.on
- Инъекции: начиная с (2.3.0+), если используется опция инъекции, она содержит разрешенные инъекции.
Функциональные компоненты гораздо дешевле рендерить, потому что они просто функции.
Они одинаково полезны в качестве компонентов обертки. Например:
- когда нам нужно программно выбрать один из нескольких других компонентов для делегирования
- когда нам нужно манипулировать дочерними элементами, реквизитами или данными перед передачей их дочернему компоненту
Ниже приведен пример компонента умного списка, который делегируется более конкретным компонентам в зависимости от переданных ему реквизитов:
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) {
// Transparently pass any attributes, event listeners, children, etc.
return createElement('button', context.data, context.children)
}
})
Когда мы передаем context.data в качестве второго аргумента createElement, мы фактически передаем любые атрибуты или прослушиватели событий, используемые на кнопке my-functions-button.
Если мы используем функциональные компоненты на основе шаблонов, нам также придется вручную добавлять атрибуты и прослушиватели. Поскольку у нас уже есть доступ к содержимому отдельного контекста, мы можем использовать data.attrs для передачи любых атрибутов HTML и прослушивателей (псевдоним для data.on) для передачи любых прослушивателей событий.
<template functional>
<button
class="btn btn-primary"
v-bind="data.attrs"
v-on="listeners"
>
<slot/>
</button>
</template>
Слоты () против детей
Можно задаться вопросом, зачем нужны слоты () и дети. Разве slots (). Default не будет таким же, как у детей? В некоторых случаях да - но что, если у нас есть функциональный компонент со следующими детьми?
<my-functional-component>
<p v-slot:foo>
first
</p>
<p>second</p>
</my-functional-component>
В этом компоненте потомки передадут нам оба абзаца, а slots (). Default даст нам только второй, а slots (). Foo даст нам только первый. Поэтому наличие дочерних элементов и slots () позволяет нам выбирать, знает ли этот компонент о системе слотов или, возможно, делегирует эту ответственность другому компоненту, передавая дочерние элементы.
Компиляция шаблонов
Возможно, нам будет интересно узнать, что шаблоны Vue на самом деле компилируются для рендеринга функций. Это деталь реализации, о которой нам обычно не нужно знать, но если мы хотим посмотреть, как скомпилированы конкретные функции шаблона, мы можем найти ее интересной.
Новый контент: Composer: менеджер зависимостей для PHP , R программирования