← Курс/Render Functions и h()#231 из 257+35 XP

Render Functions и h()

Что такое render function

Шаблон Vue компилируется в **render function** — обычную JavaScript-функцию, которая возвращает VNode (виртуальный узел DOM). В большинстве случаев шаблоны удобнее, но render functions дают полную мощь JavaScript.

Функция h()

h() (от hyperscript) создаёт VNode:

import { h } from 'vue'

// h(tag, props, children)
const vnode = h('div', { class: 'container', id: 'app' }, [
  h('h1', null, 'Заголовок'),
  h('p', { style: 'color: red' }, 'Текст'),
])

render() в setup()

import { h, ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    // Возвращаем функцию — это render function
    return () => h('div', [
      h('p', `Счётчик: ${count.value}`),
      h('button', { onClick: () => count.value++ }, '+'),
    ])
  }
}

Компоненты в h()

import MyButton from './MyButton.vue'

// h() принимает компоненты напрямую
h(MyButton, {
  label: 'Нажми',
  variant: 'primary',
  onClick: handleClick,
})

// Слоты — третий аргумент как объект
h(MyLayout, {}, {
  default: () => h('p', 'Основной контент'),
  header:  () => h('h1', 'Заголовок'),
  footer:  () => h('footer', 'Подвал'),
})

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

// Функциональный компонент — просто функция без состояния
const FancyHeading = (props, { slots }) => {
  const level = props.level || 2
  return h(`h${level}`, { class: 'fancy' }, slots.default?.())
}

// Использование в h()
h(FancyHeading, { level: 3 }, { default: () => 'Заголовок' })

JSX в Vue

Vue поддерживает JSX как альтернативу h():

// MyComponent.vue с lang="tsx"
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    return () => (
      <div class="container">
        <p>Счётчик: {count.value}</p>
        <button onClick={() => count.value++}>+</button>
      </div>
    )
  }
}

Когда использовать render functions

Используйте render functions когда:

  • Нужна программная генерация тегов (h1-h6 по данным)
  • Создаёте библиотеку компонентов с максимальной гибкостью
  • Рекурсивный рендер деревьев
  • Функциональные компоненты-обёртки
  • Оставайтесь на шаблонах когда:

  • Обычные компоненты приложения
  • Нужна читаемость для команды
  • Хотите использовать директивы (v-if, v-for и т.д.)
  • VNode — что это

    const vnode = h('p', { id: 'text' }, 'Привет')
    // Это обычный объект:
    // {
    //   type: 'p',
    //   props: { id: 'text' },
    //   children: 'Привет',
    //   el: null,       // реальный DOM-элемент после монтирования
    //   key: null,
    //   ...
    // }

    VNode — лёгкий JavaScript-объект. Vue сравнивает деревья VNode при обновлении (Virtual DOM diffing) и точечно обновляет реальный DOM.

    Примеры

    Реализация упрощённого h() и Virtual DOM — строим и рендерим дерево VNode в HTML-строку

    // Реализуем упрощённый h() и рендерер VNode → HTML.
    // Именно так работает Vue под капотом.
    
    // --- h() — создание VNode ---
    function h(type, props, children) {
      return {
        type,
        props: props || {},
        children: normalizeChildren(children),
        _isVNode: true,
      }
    }
    
    function normalizeChildren(children) {
      if (children == null) return []
      if (!Array.isArray(children)) return [children]
      return children.flat()
    }
    
    // --- Рендерер VNode → HTML строка ---
    function renderToString(vnode) {
      if (vnode == null || vnode === false) return ''
      if (typeof vnode === 'string' || typeof vnode === 'number') {
        return String(vnode)
      }
    
      const { type, props, children } = vnode
    
      // Функциональный компонент
      if (typeof type === 'function') {
        const rendered = type(props, { slots: {} })
        return renderToString(rendered)
      }
    
      // HTML-атрибуты
      const attrs = Object.entries(props)
        .filter(([k]) => !k.startsWith('on') && k !== 'key')  // игнорируем обработчики
        .map(([k, v]) => {
          if (k === 'className') k = 'class'
          if (typeof v === 'boolean') return v ? k : ''
          return `${k}="${v}"`
        })
        .filter(Boolean)
        .join(' ')
    
      const attrsStr = attrs ? ' ' + attrs : ''
    
      // Самозакрывающиеся теги
      const selfClosing = ['br', 'hr', 'img', 'input', 'link', 'meta']
      if (selfClosing.includes(type)) {
        return `<${type}${attrsStr} />`
      }
    
      const innerHtml = children.map(renderToString).join('')
      return `<${type}${attrsStr}>${innerHtml}</${type}>`
    }
    
    // --- Функциональный компонент (аналог Vue functional component) ---
    const FancyHeading = (props) => {
      const level = props.level || 2
      const text = props.text || ''
      return h(`h${level}`, { class: 'fancy', id: props.id }, text)
    }
    
    // Компонент рекурсивного дерева
    function TreeNode(props) {
      const { node } = props
      if (!node.children || node.children.length === 0) {
        return h('li', { class: 'leaf' }, node.label)
      }
      return h('li', {}, [
        h('span', { class: 'node' }, node.label),
        h('ul', {},
          node.children.map(child => TreeNode({ node: child }))
        )
      ])
    }
    
    // --- Примеры ---
    console.log('=== Базовые VNode ===')
    const vdom = h('div', { class: 'container', id: 'app' }, [
      h('h1', { class: 'title' }, 'Привет, Vue!'),
      h('p', { style: 'color: blue' }, 'Параграф'),
      h('ul', {}, [
        h('li', null, 'Один'),
        h('li', null, 'Два'),
        h('li', null, 'Три'),
      ]),
      h('br', null),
      h('input', { type: 'text', placeholder: 'Введите текст' }),
    ])
    console.log(renderToString(vdom))
    
    console.log('\n=== Функциональный компонент ===')
    const headings = h('div', {}, [
      h(FancyHeading, { level: 1, text: 'Заголовок 1', id: 'h1' }),
      h(FancyHeading, { level: 2, text: 'Заголовок 2' }),
      h(FancyHeading, { level: 3, text: 'Заголовок 3' }),
    ])
    console.log(renderToString(headings))
    
    console.log('\n=== Рекурсивное дерево ===')
    const tree = {
      label: 'Корень',
      children: [
        { label: 'Узел A', children: [
          { label: 'Лист A1', children: [] },
          { label: 'Лист A2', children: [] },
        ]},
        { label: 'Узел B', children: [
          { label: 'Лист B1', children: [] },
        ]},
      ]
    }
    const treeVdom = h('ul', { class: 'tree' }, [TreeNode({ node: tree })])
    console.log(renderToString(treeVdom))
    
    // Сравниваем два VNode (упрощённый diff)
    console.log('\n=== VNode структура ===')
    const vnode = h('p', { id: 'test' }, 'Текст')
    console.log(JSON.stringify(vnode, null, 2))