Рендер-функции и JSX
Vue рекомендует использовать шаблоны для создания приложений в подавляющем большинстве случаев. Однако бывают ситуации, когда нам нужна вся программная мощь JavaScript. Именно здесь мы можем использовать функцию render.
Если вы новичок в концепции Virtual DOM и рендер-функциях, обязательно сначала прочтите главу Механизм отрисовки.
Пример использования
Создание виртуальных узлов
Vue предоставляет функцию h()
для создания виртуальных узлов:
js
import { h } from 'vue'
const vnode = h(
'div', // тип
{ id: 'foo', class: 'bar' }, // входные параметры
[
/* дочерние элементы */
]
)
h()
— это сокращение от hyperscript, что означает «JavaScript, создающий HTML (язык разметки гипертекста)». Это имя унаследовано от соглашений, общих для многих реализаций Virtual DOM. Более описательное имя могло бы быть createVNode()
, но более короткое имя помогает, когда вам приходится вызывать эту функцию много раз в рендер-функции.
Функция h()
создана для того, чтобы быть очень гибкой:
js
// все аргументы, кроме типа, являются необязательными
h('div')
h('div', { id: 'foo' })
// В параметрах можно использовать как атрибуты, так и свойства
// Vue автоматически выбирает правильный способ назначения
h('div', { class: 'bar', innerHTML: 'привет' })
// Модификаторы параметра, такие как `.prop` и `.attr`, могут быть добавлены
// с префиксами `.` и `^` соответственно
h('div', { '.name': 'some-name', '^width': '100' })
// class и style имеют один и тот же объект/массив
// поддержка значений, которые есть в шаблонах
h('div', { class: [foo, { bar }], style: { color: 'red' } })
// слушатели событий должны передаваться как onXxx
h('div', { onClick: () => {} })
// children может быть строкой
h('div', { id: 'foo' }, 'привет')
// входные параметры могут быть опущены, если их нет
h('div', 'привет')
h('div', [h('span', 'привет')])
// массив children может содержать смешанные vnodes и строки
h('div', ['привет', h('span', 'привет')])
Полученный vnode имеет следующую форму:
js
const vnode = h('div', { id: 'foo' }, [])
vnode.type // 'div'
vnode.props // { id: 'foo' }
vnode.children // []
vnode.key // null
Примечание
Полный интерфейс VNode
содержит множество других внутренних свойств, но настоятельно рекомендуется не полагаться ни на какие другие свойства, кроме перечисленных здесь. Это позволяет избежать непреднамеренных поломок в случае изменения внутренних свойств.
Объявление рендер-функций
При использовании шаблонов с Composition API возвращаемое значение хука setup()
используется для передачи данных шаблону. Однако при использовании рендер-функций мы можем напрямую вернуть рендер-функцию:
js
import { ref, h } from 'vue'
export default {
props: {
/* ... */
},
setup(props) {
const count = ref(1)
// возвращает рендер-функцию
return () => h('div', props.msg + count.value)
}
}
Рендер-функция объявляется внутри setup()
, поэтому она, естественно, имеет доступ к входным параметрам и любому реактивному состоянию, объявленному в той же области видимости.
Помимо возврата одного vnode, вы также можете возвращать строки или массивы:
js
export default {
setup() {
return () => 'привет, мир!'
}
}
js
import { h } from 'vue'
export default {
setup() {
// используйте массив, чтобы вернуть несколько корневых узлов
return () => [
h('div'),
h('div'),
h('div')
]
}
}
Совет
Убедитесь, что вы возвращаете функцию, а не напрямую возвращаете значения! Функция setup()
вызывается только один раз для каждого компонента, в то время как возвращаемая рендер-функция будет вызвана несколько раз.
Если компоненту рендер-функции не требуется состояние экземпляра, то для краткости его можно объявить непосредственно как функцию:
js
function Hello() {
return 'привет, мир!'
}
Все верно, это действительный компонент Vue! Подробнее об этом синтаксисе см. в разделе Функциональные компоненты.
Виртуальные узлы должны быть уникальными
Все узлы в дереве компонентов должны быть уникальными. Это означает, что следующая рендер-функция недопустима:
js
function render() {
const p = h('p', 'hi')
return h('div', [
// Ух ты - дублированные vnodes!
p,
p
])
}
Если вы действительно хотите дублировать один и тот же элемент/компонент много раз, вы можете сделать это с помощью фабричной функции. Например, следующая рендер-функция - это совершенно правильный способ отображения 20 одинаковых параграфов:
js
function render() {
return h(
'div',
Array.from({ length: 20 }).map(() => {
return h('p', 'hi')
})
)
}
JSX / TSX
JSX — это XML-подобное расширение для JavaScript, которое позволяет нам писать код, подобный этому:
jsx
const vnode = <div>привет</div>
Внутри выражений JSX используйте фигурные скобки для вставки динамических значений:
jsx
const vnode = <div id={dynamicId}>привет, {userName}</div>
В create-vue
и Vue CLI есть опции для создания проектов с предварительно настроенной поддержкой JSX. Если вы настраиваете JSX вручную, обратитесь к документации @vue/babel-plugin-jsx
за подробностями.
Хотя JSX впервые появился в React, на самом деле он не имеет определённой семантики во время выполнения и может быть скомпилирован в различные варианты вывода. Если вы уже работали с JSX, обратите внимание, что преобразования Vue JSX отличается от преобразований React JSX, поэтому вы не можете использовать преобразования React JSX в приложениях Vue. Некоторые заметные отличия от React JSX включают:
- Вы можете использовать атрибуты HTML, такие как
class
иfor
, в качестве параметров — нет необходимости использоватьclassName
илиhtmlFor
. - Передача дочерних компонентов компонентам (т. е. слотов) работает по-другому.
Определение типов во Vue также обеспечивает вывод типов для использования TSX. При использовании TSX обязательно укажите "jsx": "preserve"
в tsconfig.json
, чтобы TypeScript оставлял синтаксис JSX нетронутым для обработки преобразований Vue JSX.
Вывод типов в JSX
Подобно преобразованию, JSX в Vue также требует различных определений типов.
Начиная с Vue 3.4, Vue больше не регистрирует неявно глобальное пространство имен JSX
. Чтобы указать TypeScript использовать определения типов JSX от Vue, убедитесь, что в вашем tsconfig.json
указано следующее:
json
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "vue"
// ...
}
}
Вы также можете включить это для каждого файла отдельно, добавив комментарий /* @jsxImportSource vue */
в начало файла.
Если есть код, который зависит от наличия глобального пространства имен JSX
, вы можете сохранить точное поведение глобального пространства до версии 3.4, явно ссылаясь на vue/jsx
, которое регистрирует глобальное пространство имен JSX
.
Рецепты рендер-функций
Ниже мы приведем несколько общих рецептов реализации функций шаблона в виде эквивалентных им рендер-функций / JSX.
v-if
Шаблон:
template
<div>
<div v-if="ok">да</div>
<span v-else>нет</span>
</div>
Эквивалентная рендер-функция / JSX:
js
h('div', [ok.value ? h('div', 'да') : h('span', 'нет')])
jsx
<div>{ok.value ? <div>да</div> : <span>нет</span>}</div>
v-for
Шаблон:
template
<ul>
<li v-for="{ id, text } in items" :key="id">
{{ text }}
</li>
</ul>
Эквивалентная рендер-функция / JSX:
js
h(
'ul',
// предполагается, что `items` -—это ссылка со значением массива
items.value.map(({ id, text }) => {
return h('li', { key: id }, text)
})
)
jsx
<ul>
{items.value.map(({ id, text }) => {
return <li key={id}>{text}</li>
})}
</ul>
v-on
Параметры с именами, начинающимися с on
, за которыми следует заглавная буква, рассматриваются как слушатели событий. Например, onClick
— это эквивалент @click
в шаблонах.
js
h(
'button',
{
onClick(event) {
/* ... */
}
},
'Нажми меня'
)
jsx
<button
onClick={(event) => {
/* ... */
}}
>
Нажми меня
</button>
Модификаторы событий
Для модификаторов событий .passive
, .capture
и .once
они могут быть объединены после имени события с использованием «верблюжьего» регистра.
Например:
js
h('input', {
onClickCapture() {
/* слушатель в режиме захвата */
},
onKeyupOnce() {
/* срабатывает только один раз */
},
onMouseoverOnceCapture() {
/* один раз + захват */
}
})
jsx
<input
onClickCapture={() => {}}
onKeyupOnce={() => {}}
onMouseoverOnceCapture={() => {}}
/>
Для других модификаторов событий и ключей можно использовать хелпер withModifiers
:
js
import { withModifiers } from 'vue'
h('div', {
onClick: withModifiers(() => {}, ['self'])
})
jsx
<div onClick={withModifiers(() => {}, ['self'])} />
Компоненты
Чтобы создать vnode для компонента, первым аргументом, передаваемым в h()
, должно быть определение компонента. Это означает, что при использовании рендер-функций нет необходимости регистрировать компоненты — вы можете просто использовать импортированные компоненты напрямую:
js
import Foo from './Foo.vue'
import Bar from './Bar.jsx'
function render() {
return h('div', [h(Foo), h(Bar)])
}
jsx
function render() {
return (
<div>
<Foo />
<Bar />
</div>
)
}
Как мы видим, h
может работать с компонентами, импортированными из файлов любого формата, если это корректный компонент Vue.
Динамические компоненты просты в использовании с рендер-функциями:
js
import Foo from './Foo.vue'
import Bar from './Bar.jsx'
function render() {
return ok.value ? h(Foo) : h(Bar)
}
jsx
function render() {
return ok.value ? <Foo /> : <Bar />
}
Если компонент зарегистрирован по имени и не может быть импортирован напрямую (например, глобально зарегистрирован библиотекой), его можно программно разрешить с помощью хелпера resolveComponent()
.
Отрисовка слотов
В рендер-функциях доступ к слотам можно получить из контекста setup()
. Каждый слот в объекте slots
представляет собой функцию, возвращающую массив vnodes:
js
export default {
props: ['message'],
setup(props, { slots }) {
return () => [
// слот по умолчанию:
// <div><slot /></div>
h('div', slots.default()),
// именованный слот:
// <div><slot name="footer" :text="message" /></div>
h(
'div',
slots.footer({
text: props.message
})
)
]
}
}
JSX-эквивалент:
jsx
// по умолчанию
<div>{slots.default()}</div>
// именованный
<div>{slots.footer({ text: props.message })}</div>
Передача слотов
Передача дочерних элементов компонентам работает немного иначе, чем передача дочерних элементов элементам. Вместо массива нам нужно передать либо слот-функцию, либо объект слот-функции. Функции слота могут возвращать всё, что может возвращать обычная рендер-функция, которые всегда будут нормализованы в массивы vnodes при обращении к ним в дочернем компоненте.
js
// один слот по умолчанию
h(MyComponent, () => 'привет')
// именованные слоты
// обратите внимание, что `null` требуется для того,
// чтобы объект слота не рассматривался как параметр
h(MyComponent, null, {
default: () => 'слот по умолчанию',
foo: () => h('div', 'foo'),
bar: () => [h('span', 'раз'), h('span', 'два')]
})
JSX equivalent:
jsx
// по умолчанию
<MyComponent>{() => 'привет'}</MyComponent>
// именованный
<MyComponent>{{
default: () => 'слот по умолчанию',
foo: () => <div>foo</div>,
bar: () => [<span>раз</span>, <span>два</span>]
}}</MyComponent>
Передача слотов как функций позволяет лениво вызывать их дочерним компонентом. Это приводит к тому, что зависимости слота отслеживаются дочерним слотом, а не родительским, что обеспечивает более точное и эффективное обновление.
Слоты с ограниченной областью видимости
Чтобы отобразить слот в родительском компоненте, слот передается в дочерний компонент. Обратите внимание, что теперь у слота есть параметр text
. Слот будет вызван в дочернем компоненте, а данные из дочернего компонента будут переданы в родительский компонент.
js
// родительский компонент
export default {
setup() {
return () => h(MyComp, null, {
default: ({ text }) => h('p', text)
})
}
}
Не забудьте передать null
, чтобы слоты не рассматривались как параметры.
js
// дочерний компонент
export default {
setup(props, { slots }) {
const text = ref('hi')
return () => h('div', null, slots.default({ text: text.value }))
}
}
JSX-эквивалент:
jsx
<MyComponent>{{
default: ({ text }) => <p>{text}</p>
}}</MyComponent>
Встроенные компоненты
Встроенные компоненты, такие как <KeepAlive>
, <Transition>
, <TransitionGroup>
, <Teleport>
и <Suspense>
должны быть импортированы для использования в рендер-функциях:
js
import { h, KeepAlive, Teleport, Transition, TransitionGroup } from 'vue'
export default {
setup() {
return () => h(Transition, { mode: 'out-in' } /* ... */)
}
}
v-model
Директива v-model
расширяется до параметров modelValue
и onUpdate:modelValue
на этапе компиляции шаблона — нам придётся предоставить эти параметры самостоятельно:
js
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
setup(props, { emit }) {
return () =>
h(SomeComponent, {
modelValue: props.modelValue,
'onUpdate:modelValue': (value) => emit('update:modelValue', value)
})
}
}
Пользовательские директивы
Пользовательские директивы могут быть применены к vnode с помощью withDirectives
:
js
import { h, withDirectives } from 'vue'
// пользовательская директива
const pin = {
mounted() { /* ... */ },
updated() { /* ... */ }
}
// <div v-pin:top.animate="200"></div>
const vnode = withDirectives(h('div'), [
[pin, 200, 'top', { animate: true }]
])
Если директива зарегистрирована по имени и не может быть импортирована напрямую, её можно разрешить с помощью хелпера resolveDirective
.
Ссылки на элементы шаблона
С использованием Composition API, при применении useTemplateRef()
ссылки на шаблон создаются путём передачи строкового значения как свойства в vnode:
js
import { h, useTemplateRef } from 'vue'
export default {
setup() {
const divEl = useTemplateRef('my-div')
// <div ref="my-div">
return () => h('div', { ref: 'my-div' })
}
}
Использование до 3.5
В версиях до 3.5, где функция useTemplateRef()
ещё не была введена, ссылки на шаблон создаются путём передачи самого ref()
как свойства в vnode:
js
import { h, ref } from 'vue'
export default {
setup() {
const divEl = ref()
// <div ref="divEl">
return () => h('div', { ref: divEl })
}
}
Функциональные компоненты
Функциональные компоненты — это альтернативная форма компонентов, которые не имеют собственного состояния. Они действуют как чистые функции: параметры — внутрь, виртуальные узлы — наружу. Они отображаются без создания экземпляра компонента (т. е. нет this
), и без обычных хуков жизненного цикла компонентов.
Для создания функционального компонента мы используем обычную функцию, а не объект опций. Эта функция фактически является функцией render
для компонента.
Сигнатура функционального компонента такая же, как и у хука setup()
:
js
function MyComponent(props, { slots, emit, attrs }) {
// ...
}
Большинство обычных параметров конфигурации компонентов недоступны для функциональных компонентов. Однако можно определить props
и emits
, добавив их в качестве свойств:
js
MyComponent.props = ['value']
MyComponent.emits = ['click']
Если опция props
не указана, то объект props
, переданный функции, будет содержать все атрибуты, как и attrs
. Имена параметров не будут нормализованы к «верблюжьему» регистру, если не указано свойство props
.
Для функциональных компонентов с явными props
обычный атрибут работает так же, как и с обычными компонентами. Однако для функциональных компонентов, которые явно не указывают свои props
, по умолчанию от attrs
будут наследоваться только class
, style
и onXxx
слушателей событий. В любом случае, inheritAttrs
можно установить в false
, чтобы отключить наследование атрибутов:
js
MyComponent.inheritAttrs = false
Функциональные компоненты можно регистрировать и использовать так же, как и обычные компоненты. Если вы передадите функцию в качестве первого аргумента в h()
, она будет рассматриваться как функциональный компонент.
Типизация функциональных компонентов
Функциональные компоненты могут быть типизированы в зависимости от того, являются ли они именованными или анонимными. Volar также поддерживает проверку типов правильно типизированных функциональных компонентов при их использовании в шаблонах SFC.
Именованный функциональный компонент
tsx
import type { SetupContext } from 'vue'
type FComponentProps = {
message: string
}
type Events = {
sendMessage(message: string): void
}
function FComponent(
props: FComponentProps,
context: SetupContext<Events>
) {
return (
<button onClick={() => context.emit('sendMessage', props.message)}>
{props.message}{' '}
</button>
)
}
FComponent.props = {
message: {
type: String,
required: true
}
}
FComponent.emits = {
sendMessage: (value: unknown) => typeof value === 'string'
}
Анонимный функциональный компонент
tsx
import type { FunctionalComponent } from 'vue'
type FComponentProps = {
message: string
}
type Events = {
sendMessage(message: string): void
}
const FComponent: FunctionalComponent<FComponentProps, Events> = (
props,
context
) => {
return (
<button onClick={() => context.emit('sendMessage', props.message)}>
{props.message} {' '}
</button>
)
}
FComponent.props = {
message: {
type: String,
required: true
}
}
FComponent.emits = {
sendMessage: (value) => typeof value === 'string'
}