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 userendering function

, which is closer to the compiler than templates.

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

Hello world!

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

Hello world!
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:

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, 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 therenderfunction:

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 thev-slotdirective into the component, such asHello world!## inanchored-heading#, these child nodes are stored in$slots.defaultin the component instance. If you don't understand it yet,recommend readingInstance Properties APIbefore 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:


My title

Some text content

When the browser reads this code, it will build a

"DOM node" treeto 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:

{{ blogTitle }}

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

blogTitlechanges.


Virtual DOM

Vue tracks itself by creating a

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

return createElement('h1', this.blogTitle)

createElementWhat exactly will be returned? Not actually an actual DOM element. Its more accurate name may becreateNodeDescription, 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.


createElementParameters


The next thing you need to be familiar with How to use those functions in the template in thecreateElementfunction. Here are the parameterscreateElementaccepts:

// @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 asv-bind:classandv-bind:styleare 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 asinnerHTML(this overrides thev-htmldirective).

{ // 与 `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 } 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-ifandv-for#Vue's rendering functions don't provide proprietary alternatives to operations that can be easily done in native JavaScript. For example,

v-if

andv-forused in the template:

  • {{ item.name }}

No items found.

These can be used in the rendering function using JavaScript's

if/else

andmapto 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 withv-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 thanv-model

.


Events & Key Modifiers

For.passive

,

.captureand.onceare event modifiers. Vue provides corresponding prefixes that can be used foron:

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

Modifier Handling Equivalent operations in functions .stop .prevent .self .enter , 13 .ctrl , ctrlKey
event.stopPropagation()
event.preventDefault()
if (event.target !== event.currentTarget) return Keystroke:
.13
if (event.keyCode !== 13) return(For other key modifiers, you can
Change toanother key code)Modifier key:
.alt
,.shift,.metaif (!event.ctrlKey) return(replace
were modified toaltKey,shiftKeyormetaKey)

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 throughthis.$slots. Each slot is a VNode array:

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

can also be accessed throughthis.$scopedSlotsAccess scope slots. Each scope slot is a function that returns a number of VNodes:

props: ['message'], render: function (createElement) { // `
` 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 thescopedSlotsfield in the VNode data object:

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


##JSX


If you have written a lot of

renderfunctions, 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:

 Hello world! 

This is why there is a

Babel pluginfor 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 (  Hello world!  ) } })

Using

has an alias forcreateElementis a common convention in the Vue ecosystem and is actually required by JSX. Starting from version3.4.0 of Vue's Babel plugin, we will automatically injectconst 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 ifhis 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 (noresponsive data) and no instance (Withoutthiscontext).

A

functional componentlooks 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, thepropsoption was required. In versions 2.3.0 or above, you can omit thepropsoption 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 useSingle file components, then template-based functional components can be declared like this:

Everything the component needs is passed through thecontextparameter, 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 entiredata objectpassed to the component, passed into the component as the second parameter ofcreateElement

  • 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 theinjectoption is used, then This object contains the properties that should be injected.

After addingfunctional: true, we need to update the rendering function of our anchor title component, add thecontextparameter to it, and Updatethis.$slots.defaulttocontext.childrenandthis.leveltocontext.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,dataManipulate them before passing them to child components.

The following is an example of asmart-listcomponent, 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 orintelligently 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 passingcontext.dataas the second parameter tocreateElement, we pass on all the properties and event listeners abovemy-functional-button. In fact this is so transparent that those events don't even require the.nativemodifier.

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 usingdata.attrs, or uselisteners(that is,data.onalias) to pass any event listener.


slots()andchildrenCompare

You may wonder why bothslots()andchildrenare needed. Isn'tslots().defaultsimilar tochildren? In some scenarios, this is true - but what if it's a functional component with child nodes like the following?

 

first

second

For this component,childrenwill give you two paragraph tags, whileslots().defaultwill only pass the second anonymous paragraph tag,slots().foowill pass the first named paragraph tag. You have bothchildrenandslots(), so you can choose to make the component aware of a certain slot mechanism, or simply hand it over to other components by passingchildrento 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 usingVue.compileto compile a template string on the fly: