Vue3里的render函数该怎么用?从基础到实战一次讲透
很多刚学Vue3的同学,看到文档里的render函数总会犯嘀咕:模板写组件明明很顺手,为啥还要学render?它在实际项目里到底咋用?别慌,这篇文章把Vue3 render从基础逻辑到实战技巧拆明白,不管是想搞懂原理,还是解决复杂场景开发,看完心里有数~
先搞懂:Vue3 render函数到底是做什么的?
Vue里的组件最终都会变成“虚拟DOM”(VNode)来描述真实DOM结构,模板<template>本质是“可视化的语法糖”,编译后还是会转成render函数,那手动写render函数,就是跳过模板编译这一步,直接用JS逻辑构建VNode。
举个简单对比:
用模板写按钮组件:
<template>
<button class="btn" @click="handleClick">{{ text }}</button>
</template>
用render函数实现同样逻辑(需要导入h函数):
import { h } from 'vue'
export default {
props: { text: String },
setup(props, { emit }) {
const handleClick = () => emit('click')
return () => h('button', { class: 'btn', onClick: handleClick }, props.text)
}
}
能发现render函数的特点:完全用JS逻辑控制渲染,没有模板语法的限制(比如v-if/v-for的语法规则),更适合高度动态、逻辑复杂到模板难以表达的场景。
h函数是render的“积木”,怎么玩明白?
render函数要返回VNode,而创建VNode全靠h函数(hyperscript的缩写,直译“超文本标记”),它的参数规则是:h(标签/组件, 属性对象, 子节点)
- 第一个参数:可以是html标签名(如
'button')、Vue组件(如MyComponent)、异步组件(如defineAsyncComponent(() => import('./MyComponent.vue'))) - 第二个参数:props、事件、class/style等都放在这里,注意事件要写
onXxx(比如点击事件是onClick,对应模板里的@click) - 第三个参数:子节点可以是字符串、数组(多个子节点)、嵌套的h函数
举个复杂点的例子:做一个带图标和文字的按钮组件
import { h } from 'vue'
import Icon from './Icon.vue'
export default {
setup() {
return () => h(
'button',
{ class: 'icon-btn', onClick: () => console.log('点击') },
[
h(Icon, { name: 'arrow-right' }), // 嵌套子组件
'提交' // 子节点里的字符串
]
)
}
}
如果用模板写,结构是<button><Icon name="arrow-right" />提交</button>,逻辑上两者等价,但render用JS的灵活性,可以在循环、条件判断里动态生成子节点——比如根据用户权限决定是否显示Icon:
const renderButton = (hasPermission) => {
const children = ['提交']
if (hasPermission) {
children.unshift(h(Icon, { name: 'arrow-right' }))
}
return h('button', { onClick: () => {} }, children)
}
这种“用JS逻辑动态拼装结构”的能力,就是h函数的核心价值。
什么时候该用render代替模板?这3类场景最典型
模板适合“结构稳定、逻辑简单”的组件,而render的强项是高度动态、逻辑复杂、需要JS完全掌控渲染过程的场景,这三类场景尤其适合:
场景1:动态组件切换(权限/状态驱动)
比如后台系统的导航栏,管理员和普通用户看到的菜单不同;或者根据用户选择切换不同的表单组件,用模板写需要大量v-if,但render可以用JS逻辑直接控制:
import AdminMenu from './AdminMenu.vue'
import UserMenu from './UserMenu.vue'
export default {
setup(props) {
const CurrentMenu = props.isAdmin ? AdminMenu : UserMenu
return () => h(CurrentMenu)
}
}
如果用模板,得写<AdminMenu v-if="isAdmin"/><UserMenu v-else/>,当组件数量多、切换逻辑复杂时,render的JS控制更简洁。
场景2:复杂逻辑渲染(循环+条件+动态属性)
比如做一个“动态表格”,列数、每列内容、样式都由父组件传入,用模板写需要嵌套v-for和v-if,但render可以用JS数组方法高效处理:
export default {
props: { columns: Array, data: Array },
setup(props) {
return () => h(
'table',
{},
[
h('thead', {}, [
h('tr', {}, props.columns.map(col => h('th', {}, col.title)))
]),
h('tbody', {}, props.data.map(row => h(
'tr',
{},
props.columns.map(col => h('td', { style: col.style }, row[col.key]))
)))
]
)
}
}
这里用map循环处理表头和内容,还能动态加样式,模板写法要嵌套多层v-for,代码可读性和维护性都会下降。
场景3:非DOM场景渲染(自定义渲染器)
如果要把组件渲染到Canvas、WebGL,甚至小程序端,必须用render函数配合自定义渲染器,比如做一个Canvas里的图表组件,用Vue的响应式管理数据,用render生成VNode,再由自定义渲染器把VNode转成Canvas绘图指令。
举个极简例子(思路演示,非完整代码):
import { createRenderer } from 'vue'
// 自定义渲染器:把VNode渲染到Canvas
const renderer = createRenderer({
createElement(tag) { /* 创建Canvas元素或绘图对象 */ },
insert(el, parent) { /* 把元素插入Canvas容器 */ },
// 其他必要的渲染器钩子...
})
// 用render函数写一个“Canvas文本组件”
export default {
setup() {
return () => h('text', { content: 'Hello Vue3', x: 50, y: 50 })
},
render: (instance) => {
renderer.render(instance.setupState.render(), canvasContainer)
}
}
这种“脱离浏览器DOM”的场景,模板完全派不上用场,render函数是唯一选择。
实战:用render函数写个带权限控制的导航栏
需求:导航栏菜单项由后端返回,且不同角色(admin/user)显示不同菜单;菜单项点击后跳转路由。
步骤1:拆解组件结构
导航栏 = 多个菜单项 → 每个菜单项是<a>标签(或RouterLink),带文字和图标。
步骤2:用h函数构建基础结构
先写静态结构,再加动态逻辑:
import { h } from 'vue'
import { useRouter } from 'vue-router'
import Icon from './Icon.vue'
export default {
props: { role: String, menus: Array }, // menus格式:[{ label: '首页', icon: 'home', path: '/' }, ...]
setup(props) {
const router = useRouter()
// 处理点击事件:跳转路由
const handleClick = (path) => () => router.push(path)
// 过滤有权限的菜单(假设admin能看所有,user只能看非管理类)
const filteredMenus = props.role === 'admin'
? props.menus
: props.menus.filter(menu => !menu.isAdmin)
// 生成菜单项VNode数组
const menuItems = filteredMenus.map(menu => h(
'a',
{
class: 'menu-item',
onClick: handleClick(menu.path),
style: { color: menu.disabled ? '#ccc' : '#333' }
},
[
h(Icon, { name: menu.icon }),
menu.label
]
))
return () => h('nav', { class: 'main-nav' }, menuItems)
}
}
步骤3:对比模板写法的差异
如果用模板,需要:
- 用
v-for循环menus,v-if做权限判断 - 动态绑定class、style、
@click - 嵌套
Icon组件
模板代码会是这样:
<template>
<nav class="main-nav">
<a
v-for="menu in filteredMenus"
:key="menu.path"
class="menu-item"
@click="handleClick(menu.path)"
:style="{ color: menu.disabled ? '#ccc' : '#333' }"
>
<Icon :name="menu.icon" />
{{ menu.label }}
</a>
</nav>
</template>
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import Icon from './Icon.vue'
const props = defineProps(['role', 'menus'])
const router = useRouter()
const handleClick = (path) => () => router.push(path)
const filteredMenus = computed(() => {
return props.role === 'admin'
? props.menus
: props.menus.filter(menu => !menu.isAdmin)
})
</script>
能发现:模板需要拆分<template>和<script>,用computed处理过滤,用v-for/v-if处理循环和条件;而render函数把结构和逻辑完全写在JS里,适合逻辑和结构高度耦合的场景。
进阶:JSX和render函数怎么配合?
Vue3支持JSX语法(需要安装@vitejs/plugin-vue-jsx等插件),JSX可以让render函数的写法更像HTML,可读性暴增,比如之前的按钮组件,用JSX写是这样:
import { defineComponent } from 'vue'
import Icon from './Icon.vue'
export default defineComponent({
setup(props, { emit }) {
const handleClick = () => emit('click')
return () => (
<button class="btn" onClick={handleClick}>
<Icon name="arrow-right" />
提交
</button>
)
}
})
对比h函数的写法,JSX的标签结构更直观,尤其是嵌套多层组件时,代码可读性远高于嵌套h函数。
JSX的优势场景
- 组件嵌套层级深:比如写一个表单,包含输入框、下拉框、按钮组,JSX的标签结构和HTML几乎一致,比h函数的嵌套调用清晰太多。
- 习惯React开发:JSX语法和React几乎一致,能降低学习成本,团队技术栈兼容更友好。
注意点
JSX本质还是会被编译成h函数调用,所以和h函数是“同层”技术,不是替代关系,项目里用不用JSX,看团队习惯和场景复杂度——如果组件结构复杂、团队有React背景,JSX能大幅提升开发效率。
自定义渲染器:render函数的“超纲”玩法
Vue3的createRendererAPI允许我们自定义VNode的渲染目标,比如把VNode渲染到Canvas、小程序、甚至命令行界面,这时候,render函数是“描述UI结构”的入口,自定义渲染器负责“把结构转成目标平台的渲染指令”。
案例:用自定义渲染器在Canvas里显示文字
思路:
- 用
createRenderer创建渲染器,实现createElement(创建Canvas绘图对象)、insert(把对象加入Canvas)、patchProp(更新属性,比如文字内容、位置)等钩子。 - 用render函数写一个“文本组件”,返回VNode。
- 用自定义渲染器把VNode渲染到Canvas。
核心代码(简化版):
import { createRenderer, h } from 'vue'
// 1. 定义渲染器钩子(操作Canvas)
const renderer = createRenderer({
createElement(tag) {
// tag是我们约定的标签,#39;text'代表文本对象
if (tag === 'text') {
return { type: 'text', content: '', x: 0, y: 0 }
}
},
insert(el, parent) {
// parent是Canvas上下文,el是文本对象,把el加入parent的绘制队列
parent.elements.push(el)
},
patchProp(el, key, oldValue, newValue) {
// 更新属性,比如content、x、y
el[key] = newValue
},
// 其他必要钩子(如createText、appendChild等)...
})
// 2. 用render函数写组件
const TextComponent = {
setup() {
return () => h(
'text',
{ content: 'Vue3 Render 超酷!', x: 100, y: 100 }
)
}
}
// 3. 渲染到Canvas(假设canvasCtx是Canvas 2D上下文)
const canvasCtx = document.getElementById('my-canvas').getContext('2d')
renderer.render(TextComponent.setup()(), { elements: [], ctx: canvasCtx })
// 4. 实现Canvas绘制逻辑(监听渲染器的更新,实际要做动画循环)
function renderCanvas() {
canvasCtx.clearRect(0, 0, canvas.width, canvas.height)
canvasCtx.elements.forEach(el => {
if (el.type === 'text') {
canvasCtx.fillText(el.content, el.x, el.y)
}
})
requestAnimationFrame(renderCanvas)
}
renderCanvas()
这个案例展示了render函数的扩展性——只要能自定义渲染器,Vue组件可以渲染到任何平台,这也是Vue3架构灵活性的体现。
性能优化:render函数里要避开哪些坑?
虽然render函数灵活,但写不好容易导致性能问题,这几个优化点要记牢:
避免重复创建VNode
如果组件的部分结构是静态不变的,要把这部分提取到setup外层,避免每次render都重新创建VNode。
反例(每次render都重新创建静态节点):
export default {
setup() {
return () => {
const staticDiv = h('div', { class: 'static' }, '固定内容')
return h('div', {}, [staticDiv, h('div', {}, '动态内容')])
}
}
}
正例(静态节点只创建一次):
const staticDiv = h('div', { class: 'static' }, '固定内容') // 提取到外层
export default {
setup() {
return () => h('div', {}, [staticDiv, h('div', {}, '动态内容')])
}
}
用memo优化动态子节点
当子节点是数组且大部分是静态时,用memo函数(Vue3的工具函数)缓存VNode,避免不必要的更新。
例子:动态列表里的静态项
import { h, memo } from 'vue'
const StaticItem = memo(() => h('div', {}, '我是静态项')) // 标记为静态
export default {
props: { list: Array },
setup(props) {
return () => h(
'div',
{},
[
...props.list.map(item => h('div', {}, item)),
StaticItem() // 复用静态VNode
]
)
}
}
合理拆分组件
如果render函数里逻辑太复杂,把部分逻辑拆成子组件,利用Vue的组件级更新机制(只有变化的组件会更新),减少整体渲染压力。
看完这些,你应该对Vue3 render函数的“是什么、怎么用、何时用”有了清晰画面,简单总结:模板是“可视化的快捷方式”,render是“JS层面的完全掌控”,两者不是对立而是互补,下次遇到动态组件、复杂逻辑、非DOM渲染这些场景,别慌,记得render函数和h/JSX的组合拳~要是还想深入,不妨自己写个自定义渲染器,或者用JSX重构一个复杂组件,实践出真知~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网




发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。