基础
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
14Vue.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
6render: function (createElement) {
return createElement(
'h1',
this.title
)
},
以上两种写法,在更改title
的时候,页面将会自动更新。
The Virtual DOM
Vue是通过虚拟DOM来实现这一点的,我们看这一行:1
2
3
4return 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 | // JS |
上面我们在渲染函数中实现v-model
的效果,可以看到v-model
其实是domProps:value
和input: 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 | // HTML |
从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 | // HTML |
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 | // HTML |
其实核心部分就是这样一段简单的模板代码: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 | import Vue from "vue"; |
注意
上面渲染函数的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 | // AnchoredHeading.vue |
函数式组件在封装组件的时候比较有用,比如你需要:
- 以程序代码的方式来选择其他的组件代理
- 将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
25import 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().default
和children
不相同吗?在某些情况下是的,但我们看下面这个函数式组件的节点:
使用slots().default1
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
负责代理到其他的组件。