Rendering functions & JSX
Contents
- ##Use JavaScript instead of template function JSX
- Functional components
- 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:
When starting to write a header that can only pass <anchored-heading :level="1">Hello world!</anchored-heading>
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: 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 aVirtual 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,
and v-for
used in the template:
These can be used in the rendering function using JavaScript's <ul v-if="items.length">
<li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>
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
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. For .passive
,.capture and
.once are event modifiers. Vue provides corresponding prefixes that can be used for
on:
Modifier | Prefix |
---|---|
##.passive | &
|
| !
|
| ~
|
.once.capture ~! |
| ##For example:
event.stopPropagation() |
| .prevent
event.preventDefault() |
| .self
if (event.target !== event.currentTarget) return | Keystroke: |
.13
if (event.keyCode !== 13) return (For other key modifiers, you can | 13 Change to another key code ) Modifier key: |
.alt , .shift , .meta
if (!event.ctrlKey) return (replace | ctrlKey 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 render: function (createElement) { // `<div><slot></slot></div>` return createElement('div', this.$slots.default) } can also be accessed through 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 render: function (createElement) { return createElement('div', [ createElement('child', { // 在数据对象中传递 `scopedSlots` // 格式为 { name: props => VNode | Array<VNode> } scopedSlots: { default: function (props) { return createElement('span', props.text) } } }) ]) } ##JSXIf you have written a lot of render 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> ) } }) UsingTo learn more about how JSX maps to JavaScript, read the usage documentation. Functional componentThe 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. functional functional component looks like this: Vue.component('my-component', { functional: true, // Props 是可选的 props: { // ... }, // 为了弥补缺少的实例 // 提供第二个参数作为上下文 render: function (createElement, context) { // ... } })
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
After adding 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:
The following is an example of a 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 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 <template functional> <button class="btn btn-primary" v-bind="data.attrs" v-on="listeners" > <slot/> </button> </template>
You may wonder why both <my-functional-component> <p v-slot:foo> first </p> <p>second</p> </my-functional-component> For this component, Template compilationYou 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 |