Vue系列 | 渲染函数 & JSX

基础

Vue推荐我们在大部分情况下使用templates,但有时候我们需要JavaScript的完整编程能力,此时我们可以用templates的替代品–render function

我们看一下锚定标题

1
<h1>Hello world!</h1>

对于以上HTML,我们可以使用组件接口实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<body>
<h1>Hello world!</h1>
<div id='app'>
<anchored-heading :level="3">Hello world!</anchored-heading>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<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>
<script type="text/javascript">
Vue.component('anchored-heading', {
template: '#anchored-heading-template',
props: {
level: {
type: Number,
required: true
}
}
})
new Vue({
el: '#app',
})
</script>
</body>

这个模板语法感觉并不是很漂亮,不仅啰嗦,而且我们每一个标题头标签都会重复,并且每添加一个标题头都必须执行同样的动作。

模板在大多数情况下很好,但有时候不是这样,我们用渲染函数重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level,
this.$slots.default
)
},
props: {
level: {
type: Number,
required: true
}
}
})

这种方法比较简洁,但需要熟悉Vue实例属性的一些知识。在这里你必须知道当你传递数据给没有v-slot的子组件时,属性的实例存储在$slots.default中。因此如果不写this.$slots.default,组件间的文字Hello world!是无法显示的。

Nodes,Trees,和Virtual DOM

在我们继续研究渲染函数之前,我们先来回顾一下浏览器是如何工作的:

1
2
3
4
5
<div>
<h1>Hello</h1>
Coentent
<!-- <Todo:add line> -->
</div>

浏览器会把以上代码构建一个DOM节点树,来追踪每一个节点的改变。

高效更新nodes是比较困难的,这里Vue帮我们做了这件事,我们只需要告诉Vue页面是什么就可以了。
可以写在template

1
<h1>{{ title }}</h1>

或用渲染函数:

1
2
3
4
5
6
render: function (createElement) {
return createElement(
'h1',
this.title
)
},

以上两种写法,在更改title的时候,页面将会自动更新。

The Virtual DOM

Vue是通过虚拟DOM来实现这一点的,我们看这一行:

1
2
3
4
return createElement(
'h1',
this.title
)

createElement返回的东西不是真实的DOM元素,更为形象的表示是创建节点的描述,我们将这个节点描述称为:virtual node,缩写:VNode。而Virtual DOM就是通过Vue构建的正式VNodes树。

v-model

渲染函数中没有v-model的对标物,所以我们需要自己手动实现一个:

1
2
3
4
// 在HTML
<div id='app'>
<my-input v-model="message"></my-input>
</div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// JS
<script type="text/javascript">
Vue.component('my-input', {
props:['value'],
render: function (createElement) {
var self = this
return createElement('div',[
createElement('input', {
domProps: {
value: self.value
},
on: {
input: function (event) {
self.$emit('input', event.target.value)
self.message = event.target.value
}
}
}),
createElement('p','value:' + self.message)

])
},
data () {
return {
message: ''
}
}
})

new Vue({
el: '#app',
data() {
return {
message: '',
}
}
})
</script>

Alt text

上面我们在渲染函数中实现v-model的效果,可以看到v-model其实是domProps:valueinput: function (value)相结合下的语法糖。

Slots

使用this.$slots实例属性来访问数据作为虚拟节点数组:

1
2
3
4
5
6
// JS
Vue.component('slot-div', {
render: function (createElement) {
return createElement('div', this.$slots.default)
}
})

1
2
3
// HTML
<slot-div>hello</slot-div>
// -> hello

this.$scopedSlots访问scoped slots返回VNodes作为函数:

1
2
3
4
5
6
7
8
9
10
11
// JS
Vue.component('scoped-div', {
props: ['message'],
render: function (createElement) {
return createElement('div',[
this.$scopedSlots.default({
text: this.message
})
])
}
})

1
2
3
4
5
6
7
// HTML
<scoped-div message="scopedSlotsExample">
<template slot-scope="pipe">
{{ pipe.text }}
</template>
</scoped-div>
// -> scopedSlotsExample

JSX

如果你写过许多渲染函数,你可能对这样的写法感到头疼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// JS
<script type="text/javascript">
var AnchoredHeading = Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level,
this.$slots.default
)
},
props: {
level: {
type: Number,
required: true
}
}
})
Vue.component('my-div', {
render: function (createElement) {
return createElement('anchored-heading', {
props: {
level: 1
}
}, [
createElement('span', 'Hello'),
' world!'
]
)
}
})
var vm = new Vue({
el:'#example',
})
</script>

1
2
3
4
5
// HTML
<div id="example">
<my-div></my-div>
</div>
// ->Hello world!

其实核心部分就是这样一段简单的模板代码:

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

Vue中使用了一个Babel插件去使用JSX,使语法更接近templates,让我们看一下使用JSX的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// AnchoredHeading.vue
<script>
export default {
render: function(createElement) {
return createElement("h" + this.level, this.$slots.default);
},
props: {
level: {
type: Number,
required: true
}
}
};
</script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Vue from "vue";
import AnchoredHeading from "./components/AnchoredHeading";

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

注意
上面渲染函数的h参数是必须的,其实它代表createElement,化名为h是一种使用惯例,这个是Babel 插件的3.4.0版本在任何方法和getter中自动注入的const h = this.$createElement,所以如果是以前的版本,可以去掉h,但如果版本不允许,会报错:[Vue warn]: Error in render: "ReferenceError: h is not defined"

这个JSX将代码自动映射为JavaScript代码,具体参考vuejs/jsx

函数式组件

早期我们写的锚定标题组件比较简单,它没有管理任何状态,也不用检测传递给它的状态,并且没有生命周期方法。其实,它只是写的props的函数。

像这种情况,我们可以将组件标记一个functional关键字,意味着这样的组件无状态(没有响应式数据)、无实例(没有this上下文)。函数式组件看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
// JS
Vue.component('my-component',{
// props是可选的
props: {
// ...
},
// 为了弥补缺少的实例
// 我们提供了第二个context参数
render: function (createElement, context) {
// ...
}
})

让我们来改写一下我们之前的锚定标题组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<div id="example">
<anchored-heading level="2">Hello world!</anchored-heading>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script type="text/javascript">
Vue.component('anchored-heading',{
functional: true,
props: {
level: ''
},
render: function (createElement,context) {
return createElement('h' + context.props.level, context.children)
}
})
var vm = new Vue({
el:'#example',
})
</script>
</body>
// -> Hello world!

这里面主要是添加了functional: true,渲染函数添加了context参数,context参数封装了组件的相关信息,该参数的具体内容请查看官方文档Functional Components,将this.$slots.default 更新为 context.children, 然后将 this.level 更新为 context.props.level

由于函数式组件仅仅是函数,它的渲染成本更低。

如果你使用的是单文件组件,基于函数是组件模板可以被声明为:

1
2
3
4
5
6
7
// Vue Template

import AnchoredHeading from './components/AnchoredHeading'

<template functonal>
<AnchoredHeading level="3">Hello</AnchoredHeading>
</template>

1
2
3
4
5
6
7
8
9
10
11
12
13
// AnchoredHeading.vue
<script>
export default {
name: "AnchoredHeading",
functional: true,
props: {
level: ''
},
render: function (createElement,context) {
return createElement('h' + context.props.level, context.children)
}
}
</script>

函数式组件在封装组件的时候比较有用,比如你需要:

  • 以程序代码的方式来选择其他的组件代理
  • 将children,props,或data传递到子组件之前操作他们

我们来完善一下官网的这个例子:smart-list组件根据情况代理到更具体的组件,依赖props被传递的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
先创建四个组件
*/
// EmptyList.vue
<template>
<div>
emptyList component
</div>
</template>

// TableList.vue
<template>
<div>
tableList component
</div>
</template>

// OrderList.vue
<template>
<div>
orderList component
</div>
</template>

// UnderList.vue
<template>
<div>
unorderList component
</div>
</template>

创建函数式组件SmartList来负责代理到具体组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script>
import Order from './OrderList'
import Empty from './EmptyList'
import Table from './TableList'
import Unorder from './UnorderList'
export default {
name: "SmartList",

functional: true,
props: {
items: {
type: Array,
required: true
},
isOrdered: Boolean
},
render: function (createElement, context) {
function appropriateListCompoent() {
var items = context.props.items
if (items.length === 0) return Empty
if (typeof items[0] === 'object') return Table
if (context.props.isOrdered) return Order

return Unorder
}
return createElement(
appropriateListCompoent(),
context.data,
context.children
)
}
}
</script>

在主组件中使用,根据输入数据展示对于组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import SmartList from './components/SmartList'

// HTML case1
<template>
<smart-list :items="[]"></smart-list>
</template>
// -> emptyList component

// HTML case2
<template>
<smart-list :items="[1]"></smart-list>
</template>
// -> unorderList component

// HTML case3
<template>
<smart-list :items="[1]" :is-ordered="true"></smart-list>
</template>
// -> orderList component

// HTML case4
<template>
<smart-list :items="[{'name': 'wang'}]" :is-ordered="true"></smart-list>
</template>
// -> tableList component

slots() vs children

我们可能会比较疑惑为什么同时需要slots()children,难道slots().defaultchildren不相同吗?在某些情况下是的,但我们看下面这个函数式组件的节点:

使用slots().default

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// HTML
<my-functional-component>
<template v-slot:foo>
<p>
first
</p>
</template>
<p>second</p>
</my-functional-component>

Vue.component('my-functional-component',{
render: function (createElement) {
return createElement('div', this.$slots.default)
}
})
// -> second

如果我们把上面的this.$slots.default改为this.$slots.foo,那么打印first

如果我们使用children:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// HTML
<my-functional-component>
<!-- <template v-slot:foo> -->
<p>
first
</p>
<!-- </template> -->
<p>second</p>
</my-functional-component>

// JS
Vue.component('my-functional-component',{
functional: true,
render: function (createElement, context) {
return createElement('div', context.children)
}
})
// -> first
// -> second

也就是说,children可以打印这两段,slots().default打印第二段,slots().foo打印第一段内容,所以children和slots的存在允许你去选择基于插槽系统还是通过children负责代理到其他的组件。


参考:Render Functions & JSX