15. Render Functions

Render Functions

Basics

Vue recommends using templates to build your HTML in the vast majority of cases. There are situations however, where you really need the full programmatic power of JavaScript. That’s where you can use the render function, a closer-to-the-compiler alternative to templates.

Let’s dive into a simple example where a render function would be practical. Say you want to generate anchored headings:

<h1>
  <a name="hello-world" href="#hello-world">
    Hello world!
  </a>
</h1>

For the HTML above, you decide you want this component interface:

<anchored-heading :level="1">Hello world!</anchored-heading>

When you get started with a component that just generates a heading based on the level prop, you quickly arrive at this:

<script type="text/x-template" id="anchored-heading-template">
  <div>
    <h1 v-if="level === 1">
      <slot></slot>
    </h1>
    <h2 v-if="level === 2">
      <slot></slot>
    </h2>
    <h3 v-if="level === 3">
      <slot></slot>
    </h3>
    <h4 v-if="level === 4">
      <slot></slot>
    </h4>
    <h5 v-if="level === 5">
      <slot></slot>
    </h5>
    <h6 v-if="level === 6">
      <slot></slot>
    </h6>
  </div>
</script>
Vue.component('anchored-heading', {
  template: '#anchored-heading-template',
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

That template doesn’t feel great. It’s not only verbose, but we’re duplicating <slot></slot> for every heading level and will have to do the same when we add the anchor element. The whole thing is also wrapped in a useless div because components must contain exactly one root node.

While templates work great for most components, it’s clear that this isn’t one of them. So let’s try rewriting it with a render function:

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
    }
  }
})

Much simpler! Sort of. The code is shorter, but also requires greater familiarity with Vue instance properties. In this case, you have to know that when you pass children without a slot attribute into a component, like the Hello world! inside of anchored-heading, those children are stored on the component instance at $slots.default. If you haven’t already, it’s recommended to read through the instance properties API before diving into render functions.

createElement Arguments

The second thing you’ll have to become familiar with is how to use template features in the createElement function. Here are the arguments that createElement accepts:

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // An HTML tag name, component options, or function
  // returning one of these. Required.
  'div',

  // {Object}
  // A data object corresponding to the attributes
  // you would use in a template. Optional.
  {
    // (see details in the next section below)
  },

  // {String | Array}
  // Children VNodes. Optional.
  [
    createElement('h1', 'hello world'),
    createElement(MyComponent, {
      props: {
        someProp: 'foo'
      }
    }),
    'bar'
  ]
)

The Data Object In-Depth

One thing to note: similar to how v-bind:class and v-bind:style have special treatment in templates, they have their own top-level fields in VNode data objects. This object also allows you to bind normal HTML attributes as well as DOM properties such as innerHTML (this would replace the v-html directive):

{
  // Same API as `v-bind:class`
  'class': {
    foo: true,
    bar: false
  },
  // Same API as `v-bind:style`
  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'
}

Complete Example

With this knowledge, we can now finish the component we started:

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 kebabCase 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
    }
  }
})

Constraints

VNodes Must Be Unique

All VNodes in the component tree must be unique. That means the following render function is invalid:

render: function (createElement) {
  var myParagraphVNode = createElement('p', 'hi')
  return createElement('div', [
    // Yikes - duplicate VNodes!
    myParagraphVNode, myParagraphVNode
  ])
}

If you really want to duplicate the same element/component many times, you can do so with a factory function. For example, the following render function is a perfectly valid way of rendering 20 identical paragraphs:

render: function (createElement) {
  return createElement('div',
    Array.apply(null, { length: 20 }).map(function () {
      return createElement('p', 'hi')
    })
  )
}

Replacing Template Features with Plain JavaScript

v-if and v-for

Wherever something can be easily accomplished in plain JavaScript, Vue render functions do not provide a proprietary alternative. For example, in a template using v-if and v-for:

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>

This could be rewritten with JavaScript’s if/else and map in a render function:

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-model

There is no direct v-model counterpart in render functions - you will have to implement the logic yourself:

render: function (createElement) {
  var self = this
  return createElement('input', {
    domProps: {
      value: self.value
    },
    on: {
      input: function (event) {
        self.value = event.target.value
        self.$emit('input', event.target.value)
      }
    }
  })
}

This is the cost of going lower-level, but it also gives you much more control over the interaction details compared to v-model.

Event & Key Modifiers

For the .capture and .once event modifiers, Vue offers prefixes that can be used with on:

Modifier(s) Prefix
.capture !
.once ~
.capture.once or
.once.capture
~!

For example:

on: {
  '!click': this.doThisInCapturingMode,
  '~keyup': this.doThisOnce,
  `~!mouseover`: this.doThisOnceInCapturingMode
}

For all other event and key modifiers, no proprietary prefix is necessary, because you can simply use event methods in the handler:

Modifier(s) Equivalent in Handler
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.currentTarget) return
Keys:
.enter, .13
if (event.keyCode !== 13) return (change 13 to another key code for other key modifiers)
Modifiers Keys:
.ctrl, .alt, .shift, .meta
if (!event.ctrlKey) return (change ctrlKey to altKey, shiftKey, or metaKey, respectively)

Here’s an example with all of these modifiers used together:

on: {
  keyup: function (event) {
    // Abort if the element emitting the event is not
    // the element the event is bound to
    if (event.target !== event.currentTarget) return
    // Abort if the key that went up is not the enter
    // key (13) and the shift key was not held down
    // at the same time
    if (!event.shiftKey || event.keyCode !== 13) return
    // Stop event propagation
    event.stopPropagation()
    // Prevent the default keyup handler for this element
    event.preventDefault()
    // ...
  }
}

Slots

You can access static slot contents as Arrays of VNodes from this.$slots:

render: function (createElement) {
  // <div><slot></slot></div>
  return createElement('div', this.$slots.default)
}

And access scoped slots as functions that return VNodes from this.$scopedSlots:

render: function (createElement) {
  // <div><slot :text="msg"></slot></div>
  return createElement('div', [
    this.$scopedSlots.default({
      text: this.msg
    })
  ])
}

To pass scoped slots to a child component using render functions, use the scopedSlots field in VNode data:

render (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

If you’re writing a lot of render functions, it might feel painful to write something like this:

createElement(
  'anchored-heading', {
    props: {
      level: 1
    }
  }, [
    createElement('span', 'Hello'),
    ' world!'
  ]
)

Especially when the template version is so simple in comparison:

<anchored-heading :level="1">
  <span>Hello</span> world!
</anchored-heading>

That’s why there’s a Babel plugin to use JSX with Vue, getting us back to a syntax that’s closer to templates:

import AnchoredHeading from './AnchoredHeading.vue'

new Vue({
  el: '#demo',
  render (h) {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})

Aliasing createElement to h is a common convention you’ll see in the Vue ecosystem and is actually required for JSX. If h is not available in the scope, your app will throw an error.

For more on how JSX maps to JavaScript, see the usage docs.

Functional Components

The anchored heading component we created earlier is relatively simple. It doesn’t manage any state, watch any state passed to it, and it has no lifecycle methods. Really, it’s just a function with some props.

In cases like this, we can mark components as functional, which means that they’re stateless (no data) and instanceless (no this context). A functional component looks like this:

Vue.component('my-component', {
  functional: true,
  // To compensate for the lack of an instance,
  // we are now provided a 2nd context argument.
  render: function (createElement, context) {
    // ...
  },
  // Props are optional
  props: {
    // ...
  }
})

Note: in versions <=2.3.0, the props option is required if you wish to accept props in a functional component. In 2.3.0+ you can omit the props option and all attributes found on the component node will be implicitly extracted as props.

Everything the component needs is passed through context, which is an object containing:

  • props: An object of the provided props
  • children: An array of the VNode children
  • slots: A function returning a slots object
  • data: The entire data object passed to the component
  • parent: A reference to the parent component
  • listeners: (2.3.0+) An object containing parent-registered event listeners. This is simply an alias to data.on
  • injections: (2.3.0+) if using the inject option, this will contain resolved injections.

After adding functional: true, updating the render function of our anchored heading component would simply require adding the context argument, updating this.$slots.default to context.children, then updating this.level to context.props.level.

Since functional components are just functions, they’re much cheaper to render. However, this also mean that functional components don’t show up in VueJS Chrome dev tools component tree.

They’re also very useful as wrapper components. For example, when you need to:

  • Programmatically choose one of several other components to delegate to
  • Manipulate children, props, or data before passing them on to a child component

Here’s an example of a smart-list component that delegates to more specific components, depending on the props passed to it:

var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }

Vue.component('smart-list', {
  functional: true,
  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
    )
  },
  props: {
    items: {
      type: Array,
      required: true
    },
    isOrdered: Boolean
  }
})

slots() vs children

You may wonder why we need both slots() and children. Wouldn’t slots().default be the same as children? In some cases, yes - but what if you have a functional component with the following children?

<my-functional-component>
  <p slot="foo">
    first
  </p>
  <p>second</p>
</my-functional-component>

For this component, children will give you both paragraphs, slots().default will give you only the second, and slots().foo will give you only the first. Having both children and slots() therefore allows you to choose whether this component knows about a slot system or perhaps delegates that responsibility to another component by simply passing along children.

Template Compilation

You may be interested to know that Vue’s templates actually compile to render functions. This is an implementation detail you usually don’t need to know about, but if you’d like to see how specific template features are compiled, you may find it interesting. Below is a little demo using Vue.compile to live-compile a template string:

© 2013–2017 Evan You, Vue.js contributors
Licensed under the MIT License.
https://vuejs.org/v2/guide/render-function

在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号

意见反馈
返回顶部