1.render函数
在编写vue单文件的大多数情况下,我们都是使用template模板来创建HTML。然而在一些条件判断比较复杂的场景下,使用JavaScript去描绘HTML的生成逻辑会显得更加的简洁直观。
使用Vue官网的例子来简单说明:
如果自己在开发的时候,编写的每个标题(包括h1
~h6
)都需要带锚点,如下所示:
<h1>
<a name="hello-world" href="#hello-world">
Hello world!
</a>
</h1>
如果用template
模板进行编写,会如下所示:
<template>
<h1 v-if="level === 1">
<anchor :name="name" :content="content"></anchor>
</h1>
<h2 v-else-if="level === 2">
<anchor :name="name" :content="content"></anchor>
</h2>
<h3 v-else-if="level === 3">
<anchor :name="name" :content="content"></anchor>
</h3>
<h4 v-else-if="level === 4">
<anchor :name="name" :content="content"></anchor>
</h4>
<h5 v-else-if="level === 5">
<anchor :name="name" :content="content"></anchor>
</h5>
<h6 v-else-if="level === 6">
<anchor :name="name" :content="content"></anchor>
</h6>
</template>
<script>
export default {
name:'anchor-header',
props:{
level:Number,
name:String,
content:String
},
components:{
'anchor':{
props:{
content:String,
name:String
},
template:'<a :id="name" :href="`#${name}`"> {{content}}</a>'
}
}
}
</script>
显然代码冗长累赘。但如果用render
函数来编写则如下所示:
<script>
export default {
name:'anchor-header',
props:{
level:Number,
name:String,
content:String
},
render:function(createElement){
const anchor={
props:{
content:String,
name:String
},
template:'<a :id="name" :href="`#${name}`"> {{content}}</a>'
}
const anchorEl=createElement(anchor,{
props:{
content:this.content,
name:this.name
}
})
const el=createElement(
`h${this.level}`,
[anchorEl]
)
return el
}
}
</script>
可见通过render
函数编写出的逻辑更加简洁且可读性更高。
每一个render
函数都要return
一个VNode类型的变量,是Vue中自定义的虚拟节点(virtual node),用于替换挂载元素$el。
Vue 选项中的 render 函数若存在,则 Vue 构造函数不会从 template 选项或通过 el 选项指定的挂载元素中提取出的 HTML 模板编译渲染函数。
但自己实践在vue单文件实践后发现,如果同时存在template
和render
,生成的html会以template
的逻辑为主,奇怪。
上述例子的代码可以知道,render
函数使用的场景是,要根据不同条件切换不同HTML标签,可以使用render函数。或者条件判断较多的template
中,用渲染函数编写会让代码的可读性更高的情况下,也推荐使用render
函数。
下面分析上面代码中出现的createElement
函数。
2.createElement
createElement
用于创建且return
一个VNode类型的变量(虚拟节点),以下是该函数的传入参数:
(1)一个 HTML 标签名、组件选项对象或者resolve前面两种之一类型的async函数。必填。
例如,传入'div'表示想创建一个html标签为div的VNode;如果传入'transition'代表创建transition组件。
上面作为粒子的代码就是根据传入的类型为Number
的参数level
,用模板字符串拼接成标签名称。传入level
为1则拼接出来的HTML标签名称为h1
。代表要创建html标签为h1
的VNode。
(2)所创建的VNode中所需参数为属性的数据对象。可选。
{
class,
style,
attrs,
props,
domProps,
on,
nativeOn,
directives:[
{
name,
value,
expression,
arg,
modifiers
}
],
scopedSlots:{
default:props=>createElement()
},
slot,
key,
ref,
refInFor
}
大部分属性和vue组件中存在的属性的作用一样,我就只挑几个比较特殊的属性来说明:
**nativeOn:**用于监听原生事件,而不是组件内使用。例如:
nativeOn: {click: this.nativeClickHandler}
相当于@click.native="nativeClickHandler"
scopedSlots:定义作用域插槽的内部的内容:
格式为:{ name: props => VNode | Array<VNode> }
举一个例子:
<script>
export default {
render (createElement) {
var component = {
template: `<div>
<slot></slot>
<slot name="foo"></slot>
</div>`
}
return createElement(component, {
scopedSlots: {
default: props => createElement('span', '456'),
foo: props => createElement('span', '789')
},
})
}
}
</script>
最后渲染出来的html效果如下:
<div>
<span>456</span>
<span>789</span>
</div>
scope:如果要生成的组件要插入到,需为插槽指定名称。
举个例子
<script>
export default {
render (createElement) {
var component = {
template: `<div>
<slot></slot>
<slot name="foo"></slot>
</div>`
}
const childrenEl = createElement('span', { slot: 'foo' }, '123')
return createElement(component, {
scopedSlots: {
default: props => createElement('span', '456')
// foo: props => createElement('span', '789') },
}, [childrenEl])
}
}
</script>
最后渲染出来的html效果如下:
<div>
<span>456</span>
<span>123</span>
</div>
注意:如果去掉上面例子的代码中// foo: props => createElement('span', '789')
的注释,则slot="foo"
插槽中显示内容依然为<span>789</span>
。
** refInFor:**如果你在渲染函数中给多个元素都应用了相同的 ref
名,那么 `$refs.myRef`
会变成一个数组。
(3)子级虚拟节点 (VNodes)。如果传入的是VNode则要用列Array传入,另外也可以使用字符串来生成“文本虚拟节点”。可选。
3.函数式组件
函数式组件相比于一般的vue组件而言,最大的区别是非响应式的。它不会监听任何数据,也没有实例(因此没有状态,意味着不存在诸如created,mounted的生命周期)。好处是因只是函数,故渲染开销也低很多。
把开头的例子改成函数式组件,代码如下:
<script>
export default {
name:'anchor-header',
functional:true, // 以functional:true声明该组件为函数式组件
props:{
level:Number,
name:String,
content:String
},
// 对于函数式组件,render函数会额外传入一个context参数用来表示上下文,即替代this。函数式组件没有实例,故不存在this
render:function(createElement,context){
const anchor={
props:{
content:String,
name:String
},
template:'<a :id="name" :href="`#${name}`"> {{content}}</a>'
}
const anchorEl=createElement(anchor,{
props:{
content:context.props.content, //通过context.props调用props传入的变量
name:context.props.name
}
})
const el=createElement(
`h${context.props.level}`,
[anchorEl]
)
return el
}
}
</script>
渲染函数 & JSX — Vue.js:更多关于函数式组件内容请看官网函数式组件
4.element-ui的el-row组件
最后以el-row组件的源码来分析,该源码的渲染逻辑在render
函数上,非常简洁明了:
export default {
name: 'ElRow',
componentName: 'ElRow',
props: {
tag: {
type: String,
default: 'div'
},
gutter: Number,
type: String,
justify: {
type: String,
default: 'start'
},
align: {
type: String,
default: 'top'
}
},
computed: {
style() {
const ret = {};
if (this.gutter) {
ret.marginLeft = `-${this.gutter / 2}px`;
ret.marginRight = ret.marginLeft;
}
return ret;
}
},
render(h) {
return h(this.tag, {
class: [
'el-row',
this.justify !== 'start' ? `is-justify-${this.justify}` : '',
this.align !== 'top' ? `is-align-${this.align}` : '',
{ 'el-row--flex': this.type === 'flex' }
],
style: this.style
}, this.$slots.default);
}
};
对着el-row组件传入参数的说明图来解释:
直接从render
函数处进行分析,
1.传入第一个参数为this.tag
,用于根据参数生成对应的html标签。
2.第二个参数中传入class
和style
是根据props
中的type
,gutter
,justify
,align
生成的。
3.第三个参数传入子节点。此处通过this.$slots.default
拿到传入的子节点。例如:
<el-row>
<div>123</div>
</el-row>
此时,this.$slots.default 获取的数据则是一个包含上面<div>123</div>
的VNode的数组。
以下内容来自官网:
拓展:slots() 和 children 对比
你可能想知道为什么同时需要 slots()
和 children
。slots().default
不是和 children
类似的吗?在一些场景中,是这样——但如果是如下的带有子节点的函数式组件呢?
<my-functional-component>
<p v-slot:foo>
first
</p>
<p>second</p>
</my-functional-component>
对于这个组件,children
会给你两个段落标签,而 slots().default
只会传递第二个匿名段落标签,slots().foo
会传递第一个具名段落标签。同时拥有 children
和 slots()
,因此你可以选择让组件感知某个插槽机制,还是简单地通过传递 children
,移交给其它组件去处理。