# Funciones de Renderización

Vue recomienda utilizar plantillas en la gran mayoría de los casos. Sin embargo, hay situaciones en las que necesitamos el poder programático completo de JavaScript. Ahí es donde puede utilizar la función render.

Profundicemos en un ejemplo dónde una función render() sea practical. Por ejemplo, si queremos generar enlaces de cabeceras:

<h1>
  <a name="hello-world" href="#hello-world">
    ¡Hola mundo!
  </a>
</h1>
1
2
3
4
5

Los enlaces de cabeceras son utilizado con mucha frecuencia, deberíamos crear un componente:

<anchored-heading :level="1">¡Hola mundo!</anchored-heading>
1

El componente debe generar una cabecera basada de la prop level, y lo alcanzamos con facilidad:

const { createApp } = Vue

const app = createApp({})

app.component('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>
  `,
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

Esta plantilla no se siente genial. No sólo verboso, sino también estamos repetiendo <slot></slot> par cada nivel de las cabeceras. Y cuando agregamos el elemento de enlace, tenemos que de nuevo repetirlo en cada rama de v-if/v-else-if.

Mientras plantillas funcionan muy bien para la mayoría de los componentes, es claro que este no es uno de ellos. Así que tratemos reescribirlo con una función render():

const { createApp, h } = Vue

const app = createApp({})

app.component('anchored-heading', {
  render() {
    return h(
      'h' + this.level, // nombre de la etiqueta
      {}, // props/atributos
      this.$slots.default() // matriz de componentes secundarios
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

La implementación con función render() es mucho más sencilla, pero también requiere mayor familiaridad con propiedades de instancias de componentes. En este caso, tiene que saber que cuando pasa contenidos sin directiva v-slot a un comoponente, como la ¡Hola mundo! dentro de anchored-heading, aquellos hijos son almacenado en la instancia de componente como $slots.default(). Si no está listo, se recomienda leer API de propiedades de instancia antes de profundizarse en funciones de render.

# El árbol DOM

Antes de que profundicemos en funciones de render, es importante saber un poco sobre cómo funciona los navegadores. Tomemos este código HTML como ejemplo:

<div>
  <h1>My title</h1>
  Some text content
  <!-- TODO: Agregar lema -->
</div>
1
2
3
4
5

Cuando un navegador lee este código, construye un árbol de "nodos DOM" (opens new window) para ayudarlo mantener un registro de todos.

El árbol de nodos DOM para el código HTML arriba se parece como este:

Visualización del árbol DOM

Cada elemento es un nodo. Cada pieza de texto es un nodo. ¡Incluso los comentarios son nodos! Cada nodo puede tener hijos (es decir, cada nodo puede contener otros nodos).

Actualizar todos estos nodos eficientemente puede ser difícil, pero afortunadamente, nunca tenemos que hacerlo manualmente. En cambio, informamos a Vue qué HTML queramos mostrar en la página, en una plantilla:

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

O en una función render:

render() {
  return h('h1', {}, this.blogTitle)
}
1
2
3

Y en ambos casos, Vue automáticamente mantiene la página actualizada, incluso cuando blogTitle se cambie.

# El árbol DOM virtual

Vue mantiene la página actualizada mediante construir un DOM virtual para mantener un registro de los cambios que necesite hacer al DOM real. Echemos un vistazo más cerca en esta línea:

return h('h1', {}, this.blogTitle)
1

¿Qué se retorna de la función h()? No es exactamente un elemento DOM real. Retorna un objeto plano que contiene información que describe a Vue qué tipo de nodo que se deba renderizar en la página, incluyendo descripciones de cualquier nodo hijo. La llamamos a esta descripción de nodo un "nodo virtual", a menudo abreviado a VNode. "DOM virtual" es lo que llamamos al árbol entero de VNodes, construido por un árbol de componentes Vue.

# Argumentos de h()

La función h() es una utilidad para crear VNodes. podría probablemente más precisamente ser llamado createVNode(), pero es llamado h() debido al uso frecuente y la brevedad. Acepta tres argumentos:

// @returns {VNode}
h(
  // {String | Object | Function} etiqueta
  // Un nombre de etiqueta HTML, un componente, un componente asíncrono, o un
  // componente funcional.
  //
  // Requerido.
  'div',

  // {Object} props
  // Un objeto correspondiente a los atributos, props y eventos que podríamos
  // utilizar en una plantilla
  //
  // Opcional.
  {},

  // {String | Array | Object} hijos
  // VNodes secundarios, construido por `h()`,
  // o utilizar cadenas de caracteres para obtener 'Vnodes de textos' o
  // on objeto con slots.
  //
  // Opcional.
  [
    'Va primero algo texto.',
    h('h1', 'Un titular'),
    h(MyComponent, {
      someProp: 'foobar'
    })
  ]
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

Si no hay props, luego los hijos pueden usualmente ser pasado como el segundo argumento. En casos cuando pueda tener ambigüedad, se puede pasar null como el segundo argumento para dejar los hijos como el tercero argumento.

# Ejemplo Completo

Con este conocimiento, podemos ahora terminar el componente que iniciamos:

const { createApp, h } = Vue

const app = createApp({})

/** Obtener textos de manera recursiva desde nodos hijos */
function getChildrenTextContent(children) {
  return children
    .map(node => {
      return typeof node.children === 'string'
        ? node.children
        : Array.isArray(node.children)
        ? getChildrenTextContent(node.children)
        : ''
    })
    .join('')
}

app.component('anchored-heading', {
  render() {
    // crear id en _kebab-case_ desde los contenidos de texto de los hijos
    const headingId = getChildrenTextContent(this.$slots.default())
      .toLowerCase()
      .replace(/\W+/g, '-') // reemplazar caracteres que no son palabras por un guión
      .replace(/(^-|-$)/g, '') // reemplazar guión inicial y posterior

    return h('h' + this.level, [
      h(
        'a',
        {
          name: headingId,
          href: '#' + headingId
        },
        this.$slots.default()
      )
    ])
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# Limitaciones

# VNodes deben ser únicos

Todos los VNodes en el árbol de componentes deben ser únicos. Lo que significa la siguiente función de render es inválida:

render() {
  const myParagraphVNode = h('p', 'hi')
  return h('div', [
    // ¡Caramba - VNodes duplicados!
    myParagraphVNode, myParagraphVNode
  ])
}
1
2
3
4
5
6
7

Si de verdad quiere duplicar el mismo elemento/componente múltiples veces, puede hacerlo con una función de fábrica. Por ejemplo, la siguiente función de render es una manera perfectamente válida para renderizar 20 párrafos idénticos:

render() {
  return h('div',
    Array.from({ length: 20 }).map(() => {
      return h('p', 'hi')
    })
  )
}
1
2
3
4
5
6
7

# Crear VNodes de Componentes

Para crear un VNode para un componente, el primer argumento pasado a h debe ser el componente mismo:

render() {
  return h(ButtonCounter)
}
1
2
3

Si necesitamos resolver un componente por nombre, luego podemos llamar resolveComponent:

const { h, resolveComponent } = Vue

// ...

render() {
  const ButtonCounter = resolveComponent('ButtonCounter')
  return h(ButtonCounter)
}
1
2
3
4
5
6
7
8

resolveComponent es la misma función que las plantillas utilizan internalmente para resolver componentes por nombres.

Una función render normalmente solo necesitará utilizar resolveComponent para componentes que son registados globalmente. Registración de componentes localmente puede saltarse por completo. Considere el siguiente ejemplo:

// Podemos simplificar esto
components: {
  ButtonCounter
},
render() {
  return h(resolveComponent('ButtonCounter'))
}
1
2
3
4
5
6
7

En lugar de registrar un componente por nombre y luego buscarlo, podemos utilizarlo directamente:

render() {
  return h(ButtonCounter)
}
1
2
3

# Reemplazar las características de plantillas con JavaScript plano

# v-if y v-for

Cuando algo puede ser logrado fácilmente en JavaScript plano, Las funciones de render de Vue no proporcionan una alternativa propietaria. Por ejemplo, en una plantilla que utiliza v-if y v-for:

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No se ha encontrado niguno elemento.</p>
1
2
3
4

Este podría ser reescrito con if/else y map() de JavaScript en una función render:

props: ['items'],
render() {
  if (this.items.length) {
    return h('ul', this.items.map((item) => {
      return h('li', item.name)
    }))
  } else {
    return h('p', 'No se ha encontrado niguno elemento.')
  }
}
1
2
3
4
5
6
7
8
9
10

En una plantilla puede ser útil utilizar una etiqueta <template> para sujetar una directiva v-if o v-for. Cuando se migra a función render, la etiqueta <template> es no más requerido y puede ser abandonado.

# v-model

La directiva v-model es expandido a props modelValue y onUpdate:modelValue durante la compilación de plantillas, tedrémos que proporcionar estas props por nosotros mísmos:

props: ['modelValue'],
emits: ['update:modelValue'],
render() {
  return h(SomeComponent, {
    modelValue: this.modelValue,
    'onUpdate:modelValue': value => this.$emit('update:modelValue', value)
  })
}
1
2
3
4
5
6
7
8

# v-on

Tenemos que proporcionar un nombre apropiado de prop para el manejador del evento, p. ej. para manejar eventos click, el nombre de la prop puede ser onClick.

render() {
  return h('div', {
    onClick: $event => console.log('clicked', $event.target)
  })
}
1
2
3
4
5

# Modificadores de Eventos

Para los modificadores de eventos .passive, .capture, y .once, pueden ser concatenado después del nombre de evento utilizando camelCase.

Por ejemplo:

render() {
  return h('input', {
    onClickCapture: this.doThisInCapturingMode,
    onKeyupOnce: this.doThisOnce,
    onMouseoverOnceCapture: this.doThisOnceInCapturingMode
  })
}
1
2
3
4
5
6
7

Para todos otros eventos y modificadores principales, no API específico es necesario, porque podemos utilizar métodos de eventos en el manejador:

Modificador(es) Equivalente en Manejador
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.currentTarget) return
Teclas:
e.g. .enter
if (event.key !== 'Enter') return

Cambiar 'Enter' a la tecla (opens new window) apropiada
Teclas de Modificadores:
.ctrl, .alt, .shift, .meta
if (!event.ctrlKey) return

Lo mismo para altKey, shiftKey, y metaKey

Aquí es un ejemplo con todos los modificadores utilizados juntos:

render() {
  return h('input', {
    onKeyUp: event => {
      // Abortar si el elemento que emite el evento no es el elemento al que
      // el evento sea vinculado
      if (event.target !== event.currentTarget) return
      // Abortar si la tecla que rebotó no no es la tecla _enter_
      // y la tecla _shift_ no fue presionada al mismo tiempo
      if (!event.shiftKey || event.key !== 'Enter') return
      // Dejar la propagación de evento
      event.stopPropagation()
      // Prevenir el manejador por defecto de _keyup_ de este elemento
      event.preventDefault()
      // ...
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Slots

Podemos acceder los contenidos de slot como una matriz de VNodes mediante this.$slots:

render() {
  // `<div><slot></slot></div>`
  return h('div', this.$slots.default())
}
1
2
3
4
props: ['message'],
render() {
  // `<div><slot :text="message"></slot></div>`
  return h('div', this.$slots.default({
    text: this.message
  }))
}
1
2
3
4
5
6
7

Para VNodes de componentes, necesitamos pasar los hijos a h como un objeto en lugar de una matriz. Cada propiedad es utilizado para poblar el slot del mismo nombre:

render() {
  // `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
  return h('div', [
    h(
      resolveComponent('child'),
      null,
      // pasar `slots` como el objeto de hijos
      // en forma de { name: props => VNode | Array<VNode> }
      {
        default: (props) => h('span', props.text)
      }
    )
  ])
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Los slots son pasado como funciones, permitiendo los componentes secundarios controlar la creación de los contenidos de cada slot. Cualquier dato reactivo debe ser accesado dentro de la función de slot para asegurarse de que sea registrado como una dependencia del componente hijo en vez del padre. Por el contrario, las llamadas a resolveComponent deben realizarse afuera de la función de slot, de lo contrario se resolverán respecto al componente incorrecto:

// `<MyButton><MyIcon :name="icon" />{{ text }}</MyButton>`
render() {
  // Llamadas a resolveComponent debe ser afuera de la función de slot
  const Button = resolveComponent('MyButton')
  const Icon = resolveComponent('MyIcon')

  return h(
    Button,
    null,
    {
      // Utilizar una función de flecha para preservar el valor de `this`
      default: (props) => {
        // Las propiedades reactivas deben ser leido dentro de la función
        // de _slot_ para que sean convertido en dependencias de la renderización
        // del hijo
        return [
          h(Icon, { name: this.icon }),
          this.text
        ]
      }
    }
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

Si un componente recibe slots de su padre, pueden ser pasado directamente a un componente hijo:

render() {
  return h(Panel, null, this.$slots)
}
1
2
3

Pueden también ser pasado individualmente u envuelto según proceda:

render() {
  return h(
    Panel,
    null,
    {
      // Si queremos pasar una función de slot podemos
      header: this.$slots.header,

      // Si queremos manipular el slot en alguna manera, luego necesitamos
      // envolverlo en una nueva función
      default: (props) => {
        const children = this.$slots.default ? this.$slots.default(props) : []

        return children.concat(h('div', 'Hijo adicional'))
      }
    }
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# <component> y is

Detrás de las escenas, las plantillas utiliza resolveDynamicComponent para implementar el atributo is. Podemos utilizar la misma función si necesitamos toda la flexibilidad proporcionada por is en nuestra función de render:

const { h, resolveDynamicComponent } = Vue

// ...

// `<component :is="name"></component>`
render() {
  const Component = resolveDynamicComponent(this.name)
  return h(Component)
}
1
2
3
4
5
6
7
8
9

Justo como is, resolveDynamicComponent soporta pasar un nombre de componente, un nombre de elemento HTML, u un objeto de opciones de componente.

Sin embargo, ese nivel de flexibilidad no es usualmente requerido. A menudo es posible reemplazar resolveDynamicComponent con una alternativa más directa.

Por ejemplo, si solo necesitamos soportar nombres de componentes, luego se puede utilizar resolveComponent en su lugar.

Si el VNode es siempre un elemento HTML, luego podemos pasar su nombre directamente a h:

// `<component :is="bold ? 'strong' : 'em'"></component>`
render() {
  return h(this.bold ? 'strong' : 'em')
}
1
2
3
4

De la misma manera, si el valor pasado a is es un objeto de opciones de componente, luego no necesita resolver nada, se puede pasar directamente como el primero argumento de h.

Muy parecida a una etiqueta <template>, una etiqueta <component> es solo requerido en plantillas como un marcador de posición sintáctico y debería ser descartado cuando se migre a la función render.

# Directivas Personalizadas

Se pueden aplicar directivas personalizadas a un VNode utilizando withDirectives:

const { h, resolveDirective, withDirectives } = Vue

// ...

// <div v-pin:top.animate="200"></div>
render () {
  const pin = resolveDirective('pin')

  return withDirectives(h('div'), [
    [pin, 200, 'top', { animate: true }]
  ])
}
1
2
3
4
5
6
7
8
9
10
11
12

resolveDirective es la misma función que las plantillas utilizan internalmente para resolver directivas por nombres. Eso es necesario solo si ya no tiene acceso directo al objeto de definición de la directiva.

# Componentes Integrados

Componentes integrados como <keep-alive>, <transition>, <transition-group>, y <teleport> no son registrado globalmente por defecto. Eso les permite a los empaquetadores realizar tree-shaking, así que los componentes son solo incluido en la compilación si son utilizado. Sin embargo, lo que también significa que no podemos accederlos utilizando resolveComponent o resolveDynamicComponent.

Las plantillas tienen manipulación especiales para aquellos componentes, los importan automáticamente cuando sean utilizado. Cuando estamos escribiendo nuestras propias funciones de render, necesitamos importarlos por nosotros mísmos:

const { h, KeepAlive, Teleport, Transition, TransitionGroup } = Vue

// ...

render () {
  return h(Transition, { mode: 'out-in' }, /* ... */)
}
1
2
3
4
5
6
7

# Retornar valores para funciones de render

En todos los ejemplos que hemos visto hasta el momento, la función render ha retornado un sólo VNode raíz. Sin embargo, hay alternativas.

Retornar una cadena de caracteres va creando un VNode de texto, sin cualquier elemento de envoltorio:

render() {
  return 'Hello world!'
}
1
2
3

También podemos retornar una matriz de hijos, sin envolverlos en un nodo raíz. Este creará un fragmento:

// Equivalente a la plantilla `¡Hola<br>mundo!`
render() {
  return [
    '¡Hola',
    h('br'),
    'mundo!'
  ]
}
1
2
3
4
5
6
7
8

Si un componente necesita renderizar nada, tal vez es porque el dato se está cargando, puede solo retornar null. Este va a renderizarse como un nodo de comentario en el DOM.

# JSX

Si estamos escribiendo muchos funciones de render, podría ser doloroso escribir algo como esto:

h(
  resolveComponent('anchored-heading'),
  {
    level: 1
  },
  {
    default: () => [h('span', '¡Hola'), ' mundo!']
  }
)
1
2
3
4
5
6
7
8
9

Especialmente cuando la versión de plantilla es muy conciso, en comparación:

<anchored-heading :level="1"> <span>¡Hola</span> mundo! </anchored-heading>
1

Por eso, hay un Plugin de Babel (opens new window) para utilizar JSX junto con Vue, nos llevará atrás hacia un sintaxis que sea más cerca de plantillas:

import AnchoredHeading from './AnchoredHeading.vue'

const app = createApp({
  render() {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})

app.mount('#demo')
1
2
3
4
5
6
7
8
9
10
11
12
13

Para más sobre cómo JSX se mapea a JavaScript, vea la documentación de usos (opens new window).

# Componentes Funcionales

Componentes funcionales son una forma alternativa de componentes que no posee niguno estado de sí mismo. Son renderizado sin crear una instancia de componente, ignorando el ciclo de vida normal de componentes.

Para crear un componente funcional utilizamos una función plana, en vez de un objeto de opciones. La función es realmente la runción render para el componente. Debido a que no hay referencia de this para un componente funcional, Vue lo pasa la props como el primero argumento:

const FunctionalComponent = (props, context) => {
  // ...
}
1
2
3

El segundo argumento, context, contiene tres propiedades: attrs, emit, y slots. Estos son equivalentes a las propiedades de instancias $attrs, $emit, y $slots, respectivamente.

La mayoría de las opciones de configuración normales para componentes no son disponibles para componentes funcionales. Sin embargo, es posible definir props y emits mediante agregarlos como propiedades:

FunctionalComponent.props = ['value']
FunctionalComponent.emits = ['click']
1
2

Si la opción props no es especificado, luego el objeto props pasad a la función va a contener todos atributos, lo mismo como attrs. Los nombres de prop no serán normalizado a camelCase a menos que la opción props es especificado.

Componentes funcionales se pueden registrar y consumir justo como componentes normales. Si pasa una función como el primero argumento a h, será tratado como un componente funcional.

# Compilación de Plantilla

Le puede interesar a Usted que en realidad las plantillas Vue se compilan a funciones de render. Este es un detalle de implementación no necesita por lo general, pero si le gustaría ver cómo son compilado características específicas de plantillas, le puede resultar interesante. Abajo es una pequeña demostración de compilar una cadena de caracteres de plantilla en tiempo real utilizando Vue.compile: