Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

Vue.js 2.0 Render function


May 07, 2021 Vue.js 2.0


Table of contents


Vue.js 2.0 The basis of the Render function

Vue recommends using template in most cases to create your HTML. In some scenarios, however, you really need JavaScript's full programming capabilities, which is the render function, which is closer to the compiler than template.

Let's start with a simple example of using the render function, assuming you want to generate a title with an anchor link:

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

At the HTML layer, we decided to define the component interface this way:

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

When we start writing a component that dynamically generates heading tags through level prop, you can quickly think of this implementation:

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

Template is a bit redundant in this scenario. A lthough we reuse the title label for each level, add the same anchor element to the title label. But some are wrapped in a useless div because the component must have a root node.

Although templates work well in most components, they are not very concise here. So, let's try override the example above with the render function:

Vue.component('anchored-heading', {
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // tag name 标签名称
      this.$slots.default // 子组件中的阵列
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

It's much simpler and clearer! S imply put, this makes the code a lot thin, but requires a lot of familiarity with Vue's instance properties. I n this example, you need to know that when you don't use the slot property to deliver content to a component, such as Hello world in anchored-heading, these child elements are stored in $slots.default in the component instance. If you don't know it yet, it's recommended to read the instance property API before going into the render function.

CreateElement parameter

The second thing you need to be familiar with is how to generate templates in the createElement function. Here are the parameters accepted by createElement:

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一个 HTML 标签,组件设置,或一个函数
  // 必须 Return 上述其中一个
  'div',
  // {Object}
  // 一个对应属性的数据对象
  // 您可以在 template 中使用.可选项.
  {
    // (下一章,将详细说明相关细节)
  },
  // {String | Array}
  // 子节点(VNodes). 可选项.
  [
    createElement('h1', 'hello world'),
    createElement(MyComponent, {
      props: {
        someProp: 'foo'
      }
    }),
    'bar'
  ]
)

The full data object

One thing to note: in templates, v-bind:class and v-bind:style have special processing, and they are configured at the highest level in the VNode data object.

{
  // 和`v-bind:class`一样的 API
  'class': {
    foo: true,
    bar: false
  },
  // 和`v-bind:style`一样的 API
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 正常的 HTML 特性
  attrs: {
    id: 'foo'
  },
  // 组件 props
  props: {
    myProp: 'bar'
  },
  // DOM 属性
  domProps: {
    innerHTML: 'baz'
  },
  // 事件监听器基于 "on"
  // 所以不再支持如 v-on:keyup.enter 修饰器
  // 需要手动匹配 keyCode。
  on: {
    click: this.clickHandler
  },
  // 仅对于组件,用于监听原生事件,而不是组件使用 vm.$emit 触发的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定义指令. 注意事项:不能对绑定的旧值设值
  // Vue 会为您持续追踨
  directives: [
    {
      name: 'my-custom-directive',
      value: '2'
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 如果子组件有定义 slot 的名称
  slot: 'name-of-slot'
  // 其他特殊顶层属性
  key: 'myKey',
  ref: 'myRef'
}

Full example

With this knowledge, we can now complete the components we first wanted to implement:

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

VNodes in all component trees must be unique. This means that 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 need to repeat elements/components many times, you can use factory functions to implement them. For example, the following example render function renders 20 repetitive paragraphs perfectly and efficiently:

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

Use JavaScript instead of template functionality

Whatever can be done with native JavaScript, Vue's render function does not provide a dedicated API. For example, v-if and v-for in template:

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

These are all override by JavaScript's if/else and map in the 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.')
  }
}

JSX

If you write a lot of render functions, you may feel pain:

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

Especially if the template is so simple:

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

That's why there's a Babel plugin plugin for using JSX syntax in Vue, which brings us back to syntax closer to the template.

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

Aliasing h as createElement is a common practice in the Vue ecosystem and is actually required by JSX to trigger errors in your app if h loses its role in scope.

For more information about JSX mapping to JavaScript, read the use documentation.

Functional components

The previously created anchor title component is relatively simple, with no management or monitoring of any state passed to him, and no lifecycle method. I t's just a function that receives arguments. I n this example, we mark the component as functional, which means that it is stateless (no data), no instance (no this context). A functional component is like this:

Vue.component('my-component', {
  functional: true,
  // 为了弥补缺少的实例
  // 提供第二个参数作为上下文
  render: function (createElement, context) {
    // ...
  },
  // Props 可选
  props: {
    // ...
  }
})

Everything a component needs is passed through context, including:

  • props: The object that provides props
  • Children: An array of VNode child nodes
  • slots: Slots objects
  • data: The data object passed to the component
  • parent: A reference to the parent component

After adding functional: true, a simple update between the render functions of the anchor title component adds context parameters, this.$slots.default updates to context.children, and then theis.level updates to context.props.level.

Functional components are just a function, so rendering overhead is much lower. But it also has a complete component package, which you need to know, such as:

  • Procedurally select one of several components
  • Manipulate children, props, data before passing them to sub-components.

Here's an example of a smart-list component that relies on the value of an incoming props, which represents more specific components:

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 want to know why you need slots() and children at the same time. I sn't slots().default similar to children? In some scenarios, this is the case, but what if it's a functional component and a children like this?

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

For this component, children will give you two paragraph labels, while slots(.default) will pass only the second anonymous paragraph label, and slots(.foo will pass the first named paragraph label). You have both children and slots(), so you can choose to have components distributed through the slot() system or simply received through children for other components to process.

The template is compiled

You may be interested to know that Vue's template is actually compiled into a render function. T his is an implementation detail that usually doesn't need to be cared about, but if you want to see how the functionality of the template is compiled, you'll find it very interesting. Here's a simple demo that uses Vue.compile to compile template strings in real time:

render:

function anonymous(
) {
  with(this){return _h('div',[_m(0),(message)?_h('p',[_s(message)]):_h('p',["No message."])])}
}
staticRenderFns:
_m(0): function anonymous(
) {
  with(this){return _h('h1',["I'm a template!"])}
}