Code前端首页关于Code前端联系我们

Vue3里的render函数该怎么用?从基础到实战一次讲透

terry 2周前 (10-02) 阅读数 50 #Vue
文章标签 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-forv-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循环menusv-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里显示文字

思路:

  1. createRenderer创建渲染器,实现createElement(创建Canvas绘图对象)、insert(把对象加入Canvas)、patchProp(更新属性,比如文字内容、位置)等钩子。
  2. 用render函数写一个“文本组件”,返回VNode。
  3. 用自定义渲染器把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前端网发表,如需转载,请注明页面地址。

发表评论:

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

热门