Перейти к содержанию

Рендер-функции и 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() вызывается только один раз для каждого компонента, в то время как возвращаемая рендер-функция будет вызвана несколько раз.

Мы можем объявлять рендер-функции с помощью опции render:

js
import { h } from 'vue'

export default {
  data() {
    return {
      msg: 'привет'
    }
  },
  render() {
    return h('div', this.msg)
  }
}

Функция render() имеет доступ к экземпляру компонента через this.

Помимо возврата одного vnode, вы также можете возвращать строки или массивы:

js
export default {
  render() {
    return 'привет, мир!'
  }
}
js
import { h } from 'vue'

export default {
  render() {
    // используем массив, чтобы вернуть несколько корневых узлов
    return [
      h('div'),
      h('div'),
      h('div')
    ]
  }
}

Если компоненту рендер-функции не требуется состояние экземпляра, то для краткости его можно объявить непосредственно как функцию:

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>
js
h('div', [this.ok ? h('div', 'да') : h('span', 'нет')])
jsx
<div>{this.ok ? <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>
js
h(
  'ul',
  this.items.map(({ id, text }) => {
    return h('li', { key: id }, text)
  })
)
jsx
<ul>
  {this.items.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>

В рендер-функциях доступ к слотам можно получить из this.$slots:

js
export default {
  props: ['message'],
  render() {
    return [
      // <div><slot /></div>
      h('div', this.$slots.default()),

      // <div><slot name="footer" :text="message" /></div>
      h(
        'div',
        this.$slots.footer({
          text: this.message
        })
      )
    ]
  }
}

JSX-эквивалент:

jsx
// <div><slot /></div>
<div>{this.$slots.default()}</div>

// <div><slot name="footer" :text="message" /></div>
<div>{this.$slots.footer({ text: this.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' } /* ... */)
  }
}
js
import { h, KeepAlive, Teleport, Transition, TransitionGroup } from 'vue'

export default {
  render() {
    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)
      })
  }
}
js
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  render() {
    return h(SomeComponent, {
      modelValue: this.modelValue,
      'onUpdate:modelValue': (value) => this.$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 })
  }
}

С помощью Options API ссылки на элементы шаблона создаются путём передачи имени ссылки в виде строки в параметре vnode:

js
export default {
  render() {
    // <div ref="divEl">
    return h('div', { ref: 'divEl' })
  }
}

Функциональные компоненты

Функциональные компоненты — это альтернативная форма компонентов, которые не имеют собственного состояния. Они действуют как чистые функции: параметры — внутрь, виртуальные узлы — наружу. Они отображаются без создания экземпляра компонента (т. е. нет this), и без обычных хуков жизненного цикла компонентов.

Для создания функционального компонента мы используем обычную функцию, а не объект опций. Эта функция фактически является функцией render для компонента.

Сигнатура функционального компонента такая же, как и у хука setup():

js
function MyComponent(props, { slots, emit, attrs }) {
  // ...
}

Поскольку для функционального компонента не существует ссылки this, Vue передаст props в качестве первого аргумента:

js
function MyComponent(props, context) {
  // ...
}

Второй аргумент, context, содержит три свойства: attrs, emit и slots. Они эквивалентны свойствам экземпляра $attrs, $emit и $slots соответственно.

Большинство обычных параметров конфигурации компонентов недоступны для функциональных компонентов. Однако можно определить 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'
}
Рендер-функции и JSX