Rendering functions & JSX


Contents

  • Basics

  • Nodes, trees and virtual DOM

  • ##createElement Parameters

    • Drill down into the data object

    • Complete example

    • Constraints

  • ##Use JavaScript instead of template function

    • v-if and v-for

    • ##v-model
    • Events & Key Modifiers
    • Slots

  • JSX
  • Functional components
    • Passing attributes and events to child elements or child components
    • slots() and children comparison
  • Template compilation


##Basic

Vue recommends using templates to create your HTML in most cases. However, there are some scenarios where you really need the full programming capabilities of JavaScript. At this time you can use rendering function

, which is closer to the compiler than templates.

Let's dive into a simple example where the render
function is very useful. Suppose we want to generate some anchored titles:

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

For the HTML above, you decide to define the component interface like this:

<anchored-heading :level="1">Hello world!</anchored-heading>
When starting to write a header that can only pass

level

When prop dynamically generates a heading (heading) component, you may quickly think of implementing it like this:

<script type="text/x-template" id="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>
</script>
Vue.component('anchored-heading', {
  template: '#anchored-heading-template',
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

Using a template is not the best choice here: not only is the code lengthy, but it is also repeated in each level of heading. Write <slot></slot>, and repeat it again when you want to insert the anchor element.

Although templates are very useful in most components, it is obviously not suitable here. So, let’s try to rewrite the above example using the render function:

Vue.component('anchored-heading', {
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // 标签名称
      this.$slots.default // 子节点数组
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

Looks much simpler! This way the code is much simpler, but you need to be very familiar with Vue's instance properties. In this example, you need to know that when passing child nodes without the v-slot directive into the component, such as Hello world!## in anchored-heading #, these child nodes are stored in $slots.default in the component instance. If you don't understand it yet, recommend readingInstance Properties API before diving into the rendering function.


Nodes, trees, and virtual DOM


Before we dive into the rendering function, let’s understand a few How the browser works is important. Take the following HTML as an example:


<div>
  <h1>My title</h1>
  Some text content
  <!-- TODO: Add tagline -->
</div>

When the browser reads this code, it will build a

"DOM node" tree to keep track of all content, just like you Like drawing a family tree to trace the development of family members.

The DOM node tree corresponding to the above HTML is as shown below:

1.png

Each element is a node. Each piece of text is also a node. Even comments are nodes. A node is a part of a page. Just like a family tree, each node can have children (that is, each part can contain other parts).

Updating all these nodes efficiently can be difficult, but fortunately you don't have to do it manually. You just need to tell Vue what HTML you want on the page, this can be in a template:

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

or in a render function:

render: function (createElement) {
  return createElement('h1', this.blogTitle)
}

In both cases, Vue The page will be automatically kept updated, even if

blogTitle changes.


Virtual DOM

Vue tracks itself by creating a

Virtual DOM How to change the real DOM. Please look at this line of code carefully:

return createElement('h1', this.blogTitle)

createElement What exactly will be returned? Not actually an actual DOM element. Its more accurate name may be createNodeDescription, because the information it contains will tell Vue what kind of node needs to be rendered on the page, including the description information of its child nodes. We describe such a node as a "virtual node", and often abbreviate it as "VNode". "Virtual DOM" is what we call the entire VNode tree built from the Vue component tree.


createElement Parameters


The next thing you need to be familiar with How to use those functions in the template in the createElement function. Here are the parameters createElement accepts:

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一个 HTML 标签名、组件选项对象,或者
  // resolve 了上述任何一种的一个 async 函数。必填项。
  'div',
  // {Object}
  // 一个与模板中属性对应的数据对象。可选。
  {
    // (详情见下一节)
  },
  // {String | Array}
  // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)


Deep into the data object

One thing to note: just as v-bind:class and v-bind:style are treated specially in the template syntax, they also have corresponding counterparts in the VNode data object top-level fields. This object also allows you to bind normal HTML attributes, as well as DOM attributes such as innerHTML (this overrides the v-html directive).

{
  // 与 `v-bind:class` 的 API 相同,
  // 接受一个字符串、对象或字符串和对象组成的数组
  'class': {
    foo: true,
    bar: false
  },
  // 与 `v-bind:style` 的 API 相同,
  // 接受一个字符串、对象,或对象组成的数组
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 普通的 HTML 特性
  attrs: {
    id: 'foo'
  },
  // 组件 prop
  props: {
    myProp: 'bar'
  },
  // DOM 属性
  domProps: {
    innerHTML: 'baz'
  },
  // 事件监听器在 `on` 属性内,
  // 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
  // 需要在处理函数中手动检查 keyCode。
  on: {
    click: this.clickHandler
  },
  // 仅用于组件,用于监听原生事件,而不是组件内部使用
  // `vm.$emit` 触发的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
  // 赋值,因为 Vue 已经自动为你进行了同步。
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 作用域插槽的格式为
  // { name: props => VNode | Array<VNode> }
  scopedSlots: {
    default: props => createElement('span', props.text)
  },
  // 如果组件是其它组件的子组件,需为插槽指定名称
  slot: 'name-of-slot',
  // 其它特殊顶层属性
  key: 'myKey',
  ref: 'myRef',
  // 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
  // 那么 `$refs.myRef` 会变成一个数组。
  refInFor: true
}


Full Example

With this knowledge, we can now accomplish what we originally wanted to achieve Components:

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) {
    // 创建 kebab-case 风格的 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
    }
  }
})


##Constraint

VNode must be unique

All VNodes in the component tree must be unique. This means that the following rendering function is illegal:

render: function (createElement) {
  var myParagraphVNode = createElement('p', 'hi')
  return createElement('div', [
    // 错误 - 重复的 VNode
    myParagraphVNode, myParagraphVNode
  ])
}

If you really need to repeat an element/component many times, you can use a factory function to achieve it. For example, the following rendering function renders 20 identical paragraphs in a completely legal way:

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


Using JavaScript instead of template functions



##v-if and v-for#Vue's rendering functions don't provide proprietary alternatives to operations that can be easily done in native JavaScript. For example,

v-if

and v-for used in the template:

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>
These can be used in the rendering function using JavaScript's

if/else

and map to rewrite:

props: ['items'],
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-modelThere is no direct correspondence with v-model

in the rendering function - you must implement the corresponding logic yourself:

props: ['value'],
render: function (createElement) {
  var self = this
  return createElement('input', {
    domProps: {
      value: self.value
    },
    on: {
      input: function (event) {
        self.$emit('input', event.target.value)
      }
    }
  })
}
This is the price of going deep into the bottom layer, but This gives you more control over interaction details than v-model

.


Events & Key Modifiers

For .passive

,

.capture and .once are event modifiers. Vue provides corresponding prefixes that can be used for on:

##.capture!.once~##.capture.once or##For example:
on: {
  '!click': this.doThisInCapturingMode,
  '~keyup': this.doThisOnce,
  '~!mouseover': this.doThisOnceInCapturingMode
}
For all other modifiers, the private prefix is ​​not necessary because you can use the event method in the event handler:
ModifierPrefix
##.passive &
.once.capture
~!

ModifierHandling Equivalent operations in functions.stop.prevent.self.enter, 13.ctrl, ctrlKey
event.stopPropagation()
event.preventDefault()
if (event.target !== event.currentTarget) returnKeystroke:
.13
if (event.keyCode !== 13) return (For other key modifiers, you can
Change to another key code)Modifier key:
.alt
, .shift, .metaif (!event.ctrlKey) return (replace
were modified to altKey, shiftKey or metaKey)

Here is an example using all modifiers:

on: {
  keyup: function (event) {
    // 如果触发事件的元素不是事件绑定的元素
    // 则返回
    if (event.target !== event.currentTarget) return
    // 如果按下去的不是 enter 键或者
    // 没有同时按下 shift 键
    // 则返回
    if (!event.shiftKey || event.keyCode !== 13) return
    // 阻止 事件冒泡
    event.stopPropagation()
    // 阻止该元素默认的 keyup 事件
    event.preventDefault()
    // ...
  }
}


slot

you The contents of static slots can be accessed through this.$slots. Each slot is a VNode array:

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

can also be accessed through this.$scopedSlots Access scope slots. Each scope slot is a function that returns a number of VNodes:

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

If you want to use the rendering function to transfer effects to subcomponents Domain slots, you can use the scopedSlots field in the VNode data object:

render: function (createElement) {
  return createElement('div', [
    createElement('child', {
      // 在数据对象中传递 `scopedSlots`
      // 格式为 { name: props => VNode | Array<VNode> }
      scopedSlots: {
        default: function (props) {
          return createElement('span', props.text)
        }
      }
    })
  ])
}


##JSX


If you have written a lot of

render functions, you may find the following code very painful to write:

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

Especially the corresponding template. In the simple case:

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

This is why there is a

Babel plugin for using JSX syntax in Vue, which can get us back to a syntax closer to templates.

import AnchoredHeading from './AnchoredHeading.vue'

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

Using

h as an alias for createElement is a common convention in the Vue ecosystem and is actually required by JSX. Starting from version 3.4.0 of Vue's Babel plugin, we will automatically inject const h = in any method and getter containing JSX declared in ES2015 syntax (not in functions or arrow functions) this.$createElement, so you can remove the (h) parameter. For earlier versions of the plugin, the application will throw an error if h is not available in the current scope.

To learn more about how JSX maps to JavaScript, read the

usage documentation.


Functional component


The anchor title component created before is relatively simple. It does not manage any state, does not listen to any state passed to it, and has no lifecycle methods. In fact, it's just a function that accepts some props.


In such a scenario, we can mark the component as

functional, which means it is stateless (no responsive data) and no instance (Without this context).

A

functional component looks like this:

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

Note: In versions prior to 2.3.0, if a functional component wanted to receive props, the props option was required. In versions 2.3.0 or above, you can omit the props option and all properties on the component will be automatically and implicitly resolved to props.

When using functional components, the reference will be HTMLElement because they are stateless and instanceless.

In versions 2.5.0 and above, if you use Single file components, then template-based functional components can be declared like this:

<template functional>
</template>

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

  • props: Provided Objects of all props

  • children: Array of VNode child nodes

  • slots: one Function that returns an object containing all slots

  • scopedSlots: (2.6.0) An object that exposes the passed scoped slots. Also exposes normal slots as functions.

  • data: The entire data object passed to the component, passed into the component as the second parameter of createElement

  • parent: Reference to the parent component

  • listeners: (2.3.0 ) An object containing all event listeners registered by parent components for the current component. This is an alias for data.on.

  • injections: (2.3.0 ) If the inject option is used, then This object contains the properties that should be injected.

After adding functional: true, we need to update the rendering function of our anchor title component, add the context parameter to it, and Update this.$slots.default to context.children and this.level to context.props.level.

Because functional components are just functions, the rendering overhead is much lower.

They are also very useful as wrapper components. For example, when you need to do this:

  • Programmatically select one of multiple components to render on your behalf;

  • In the next stepchildren, props, data Manipulate them before passing them to child components.

The following is an example of a smart-list component, which can render more specific components based on the value of the incoming prop:

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

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


Pass attributes and events to child elements or child components

In ordinary components, attributes that are not defined as props will be automatically added to the root element of the component, replacing existing attributes with the same name or intelligently merging them.

Functional components, however, require you to explicitly define this behavior:

Vue.component('my-functional-button', {
  functional: true,
  render: function (createElement, context) {
    // 完全透传任何特性、事件监听器、子节点等。
    return createElement('button', context.data, context.children)
  }
})

By passing context.data as the second parameter to createElement , we pass on all the properties and event listeners above my-functional-button. In fact this is so transparent that those events don't even require the .native modifier.

If you use template-based functional components, then you will also need to add attributes and listeners manually. Because we have access to its independent context, we can pass any HTML attribute using data.attrs, or use listeners (that is, data.on alias) to pass any event listener.

<template functional>
  <button
    class="btn btn-primary"
    v-bind="data.attrs"
    v-on="listeners"
  >
    <slot/>
  </button>
</template>


slots() and children Compare

You may wonder why both slots() and children are needed. Isn't slots().default similar to children? In some scenarios, this is true - but what if it's a functional component with child nodes like the following?

<my-functional-component>
  <p v-slot:foo>
    first
  </p>
  <p>second</p>
</my-functional-component>

For this component, children will give you two paragraph tags, while slots().default will only pass the second anonymous paragraph tag, slots().foo will pass the first named paragraph tag. You have both children and slots(), so you can choose to make the component aware of a certain slot mechanism, or simply hand it over to other components by passing children to deal with.


Template compilation


You may be interested to know that Vue’s templates actually Compiled into rendering functions. This is an implementation detail and usually nothing to care about. But if you want to see how the functionality of the template is compiled, you might find it very interesting. Here's a simple example of using Vue.compile to compile a template string on the fly: