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的createRenderer
API允许我们自定义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前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。