背景
最近在弄一个可视化后台平台,需要实现一个递归组件,已是便使用template模板写了一个loopComponent.vue的递归组件,写完之后发现并不能满足需求,原因主要是因为使用template模板在递归时总会多出一个父级容器,请往下看例子。
template版递归组件
- LoopComponent.vue文件
请留意下面的
class="aaaaaaaaaaaa"
<template>
<!-- 注意此处的aaaaaaaaaaaaa 待会儿会说到 -->
<div class="aaaaaaaaaaaaa">
<div v-for="(item, index) in list" :key="index" class="list-item">
<div class="item-name">
<span>{{ item.name }}</span>
</div>
<template v-if="item.children">
<loop-component :list="item.children" class="children-item" />
</template>
</div>
</div>
</template>
<script>
export default {
name: "loopComponent",
props: {
list: Array
}
};
</script>
- main.vue 文件
<template>
<div class="list-detail">
<loop-component :list="list"></loop-component>
</div>
</template>
<script>
export default {
list: [{
name: "广东",
children: [{
name: "广州",
children: [{
name: "天河区"
},
{
name: "黄浦区"
}]
},{
name: "深圳",
children: [{
name: "福田区"
},
{
name: "南山区"
}]
}]
}]
}
</script>
-
渲染后的页面如下
从渲染的页面来上看着似乎没什么问题,但是事实是使用template模板渲染的html结构上却会存在一些弊端,请记住上面标注的class="aaaaaaaaaaaa",现在让我们F12看看它在哪里。
- F12下渲染后的html结构
从截图可以看出,
class="aaaaaaaaaaaa"出现了三次。递归组件在每层次递归都会把最外层的div添加进去,虽然看着多了一层没什么问题,但是随着一些有使用到slot插槽的组件时,在父子组件或同级组件会有数据间的关联,如果使用template版递归组件的话,由于多了一层div,原本渲染后的元素层级由父子或同级层级变成了爷孙层级,从而导致一些意想不到的隐藏BUG(找起来真要命,亲身经历。。。),这不是我们想要的。有机智的小伙伴可能会想到,把外层<div class="aaaaaaaaaaaaa">去掉,这样的话,Vue会给你一个大大的提示:Cannot use v-for on stateful component root element because it renders multiple elements,翻译过来就是不能再根元素使用v-for指令。其实原因也很简单,在Vue2版本里,template模板就只能存在一个根元素,随着只写了一个div,但是有了v-for指令就是有多个根元素了,所以是不允许的(Vue3除外哈哈哈)。
想要解决这个问题,就需要引入我们的主角渲染函数render了。
渲染函数render
什么是渲染函数render? 其实在Vue的官方文旦就已经说的很详细了 cn.vuejs.org/v2/guide/re… 。
Vue模板编译
在大多数开发场景下,我们都是使用Vue提供的模板语法来声明式地描述状态和DOM之间的绑定关系,然后通过模板来生成真实DOM并呈现在浏览器上。其中vue在拿到我们的模板时,会将其编译成渲染函数,最后执行完渲染函数就得到了VNODE,这就是Vue模板编译的大致流程。
既然我们不能再template模板中去掉最外层的div,那么我们就从渲染函数render中入手。
递归组件 渲染函数版本
- LoopComponent.js 文件
export default {
name: 'LoopComponent',
props: {
list: {
type: Array,
default: () => {
return []
}
}
},
methods: {
loopH(h, list) {
return h(
'div',
{
attrs: {
class: 'list-item'
},
props: {
list: list
}
},
[
h('div', {
attrs: {
class: 'item-name'
}
}, [
h('span', list.name)
]),
(() => {
if (!list.children || !list.children.length) return []
return list.children.map((item) => {
return this.loopH(h, item)
})
})()
]
)
}
},
mounted() {
},
render(h) {
return this.loopH(h, this.list[0])
}
}
我的render方法有点小特殊,额外封装了个递归方法的loopH()返回vnode对象。
- main.vue 页面
<template>
<div class="list-detail">
<loop-component :list="list"></loop-component>
</div>
</template>
<script>
export default {
list: [{
name: "广东",
children: [{
name: "广州",
children: [{
name: "天河区"
},
{
name: "黄浦区"
}]
},{
name: "深圳",
children: [{
name: "福田区"
},
{
name: "南山区"
}]
}]
}]
}
</script>
render递归组件和template递归组件的html结构对比
- template版本
- render版本
从上面两个结构html结构图看出,render版本的已经不存在额外的元素,渲染的每个元素都是我们所想要的,这样就很nice,完美。
总结
虽然使用render函数能渲染出更符合开发人员预期的html结构、减少html层级关系,但是其代码写起来并没有template模板那么通俗易懂(当然,写习惯的大佬例外....),两者各有利弊,按实际场景使用,个人感觉只有在template模板实现不了的需求才会去用render函数去实现。