前言
最近有一位校招生入职两个多月了,开始参与组件库封装工作,其中就包含了 <overflow-list>
和 <space>
组件,这两个组件本身并不难。
他在实现过程中遇到了一些困惑,特别是对于何时该使用 render 函数感到迷茫。
我建议他先参考其他成熟组件库的实现方式,并结合 render 函数来完成需求。
他又问:那什么时候该用 render 函数?

什么时候该用 render 函数?
在 Vue 中,render
函数是一种用 JavaScript 代码来描述组件渲染内容的方式,它是 Vue 模板编译后的底层实现形式。
简单来说,template
模板最终会被编译成 render
函数,而 render
函数的执行结果会生成虚拟 DOM(VNode),Vue 再根据虚拟 DOM 渲染出真实 DOM。

如果你有如下三种场景,那么你就可以考虑使用 render 函数来实现。
需要更高的灵活性和控制权时
模板语法是声明式的,它描述的是"在某种状态下,视图应该是什么样子"。
而 Render 函数是命令式的,它用 JavaScript 的完整能力来"命令"Vue 如何构建视图。
这种根本性的差异使得后者对 VNode 具有极致控制性和灵活性。
比如前面提到的 <overflow-list>
就符合这个场景,<space>
组件比较简单,不过多介绍。
<overflow-list>
折叠列表
- 作用 :根据当前的宽度下动态判断能正常展示多少个子组件,无法展示的子组件个数通过一个 tag 来表示被收起

-
用法 :
html<overflow-list> <div v-for="item of tags" :key="item">Tag{{item}}</div> <template #overflow> <div>自定义tag</div> </template> </verflow-list>
组件内部需要做什么?

从组件的用法上来看,在组件内部需要做下面几件事:
- 获取 默认插槽 中的所有子节点,为不同子节点间设置间隔 margin
- 子节点间要实现两两间隔,可以使用 margin 和 padding 实现,但最好不要直接修改子节点上的样式,应该要为每个子节点包裹一层
<div class="overflow-list-item">
容器,所有的样式变更都在它上面设置
- 子节点间要实现两两间隔,可以使用 margin 和 padding 实现,但最好不要直接修改子节点上的样式,应该要为每个子节点包裹一层
- 当父容器宽度 发生变化时,计算当前最大能够显示子节点的个数,其他的通过 tag 的方式,表示还有剩余未展示
- 可以通过
ResizeObserver API
实现对当前容器宽度的监听,然后进行计算
- 可以通过
- 支持通过 from 设置折叠方向,意味着 自定义折叠元素 的位置会发生变化
- 通过 overflow 插槽 支持外部自定义折叠元素
基于以上要完成的内容,如果直接使用 template
模版是不好实现的,render
函数版本如下:
js
<script>
import { h, onBeforeUnmount, onMounted, ref } from 'vue'
export default {
name: 'OverflowList',
props: {
from: {
type: String,
default: 'end',
},
margin: {
type: Number,
default: 8,
},
},
emits: ['change'],
setup(props, { slots, emit }) {
const showCount = ref(Infinity)
let childrenWidths = []
const overflowListRef = ref(null)
const initObserve = (el, callback) => {
// 保存初始子节点宽度
childrenWidths = Array.from(el.children).map((child) => child.clientWidth)
const resizeObserver = new ResizeObserver(callback)
resizeObserver.observe(el)
onBeforeUnmount(() => resizeObserver.unobserve(el))
}
const resizeAction = debounce(() => {
const parentWidth = overflowListRef.value.clientWidth
let count = 0,
currentWidth = 0
for (const childrenWidth of childrenWidths) {
currentWidth += childrenWidth
if (currentWidth < parentWidth) {
count++
} else {
break
}
}
showCount.value = count
emit('change', showCount.value)
})
onMounted(() => {
initObserve(overflowListRef.value, resizeAction)
})
return () => {
console.log('render')
// 获取默认插槽子节点集合,作为初始值
let newChildren = slots.default()[0].children.slice()
// 计算出折叠元素的个数
const overflowCount = newChildren.length - showCount.value
let hasReverse = false
// 有折叠元素,才需要 tag
if (overflowCount > 0) {
const overflowChild = slots.overflow
? slots.overflow()[0]
: h('div', { class: 'overflow-tag' }, `+${overflowCount + 1}`)
// 定义 overflow 元素位置
if (props.from === 'start') {
newChildren.push(overflowChild)
// 翻转是为了方便下面的判断
newChildren.reverse()
hasReverse = true
} else {
newChildren.unshift(overflowChild)
}
}
// 根据 showCount 值计算能够显示多少子元素,多余的就不需要渲染
newChildren = newChildren.filter((child, index) => {
if (index + 1 > showCount.value) return null
return child
})
// 处理完成,恢复子节点顺序
if (hasReverse) {
newChildren.reverse()
}
// 为每个子节点包裹一层容器,并设置间距
newChildren = newChildren.map((child, index) => {
const margin = (index + 1 < newChildren.length ? props.margin : 0) + 'px'
return h(
'div',
{
class: 'overflow-list-item',
style: {
marginRight: margin,
},
},
child,
)
})
// 最终渲染
return h('div', { class: 'overflow-list', ref: overflowListRef }, newChildren)
}
},
}
</script>
效果如下:

在运行时,动态生成模板时
如果你在某个 js、ts 文件中,想要在运行某段逻辑之后要展示某些视图(如 Notification ),并且想要自定义其对应的 title、content、footer 时,也可以使用 render 函数来实现。
如下:
js
import { h } from 'vue'
import HandTitle from './HandTitle.vue'
import HandContent from './HandContent.vue'
function action(data) {
Notification.info({
id,
showIcon: false,
position: 'bottomLeft',
closable: false,
duration: 0,
style: { width: '260px' },
title: () => h(HandTitle, {
getName: getShowName(),
}),
content: () => h(HandContent, {
onOk() {},
onRefuse() {},
})
});
}
此处使用 render 函数的好处在于:
- 可以在 JavaScript 逻辑中直接创建组件实例
- 支持动态传递 props 和事件处理函数
- 避免在模板中编写复杂的条件渲染逻辑
需要复杂的条件渲染内容时
当条件渲染逻辑非常复杂,使用模板会导致代码难以维护时,适用场景有:
-
多重嵌套的条件渲染
-
基于复杂业务逻辑的视图渲染
-
需要编程式生成大量相似但略有不同的元素
比如实现最常见 <h1>、<h2>、<h3>、<h4>、<h5>、<h6>
的组件效果。
template 模版需要如下的实现:
js
<template>
<h1 v-if="level === 1">{{ title }}</h1>
<h2 v-else-if="level === 2">{{ title }}</h2>
<h3 v-else-if="level === 3">{{ title }}</h3>
<h4 v-else-if="level === 4">{{ title }}</h4>
<h5 v-else-if="level === 5">{{ title }}</h5>
<h6 v-else-if="level === 6">{{ title }}</h6>
</template>
而 render 函数只需要如下的实现:
js
<script>
import { h } from 'vue'
export default {
name: 'Title',
props: {
level: {
type: Number,
default: 1,
},
title: {
type: String,
default: '',
},
},
setup(props, context) {
return () => {
h(`h${props.level}`, {}, props.title)
}
},
}
</script>
总结
render 函数是 Vue 提供的强大工具,它在以下场景中特别有用:
- 需要高度控制组件渲染逻辑时
- 需要根据运行时数据动态生成组件结构时
- 需要实现复杂的条件渲染时
- 开发可复用组件库时
对于刚接触 Vue 的开发者,建议先从模板语法开始,当遇到模板无法优雅解决的问题时,再考虑使用 render 函数。
随着经验的积累,你会逐渐体会到 render 函数在复杂场景下的价值。
希望这篇文章能帮助你更好地理解 Vue render 函数的应用场景和使用方法。