Vue 3 高级组件设计与渲染函数实战 2026 | 组件设计模式完全指南

当模板语法无法满足复杂需求时,Vue 3 的渲染函数和组合式 API 提供了更强大的组件设计能力。本文将深入渲染函数、高阶组件、自定义指令等高级特性,掌握构建灵活可复用组件的设计模式。
一、渲染函数与 h 函数
1.1 基本概念
渲染函数是 Vue 组件的底层实现,h 函数用于创建虚拟 DOM 节点(VNode):
typescript
import { h } from 'vue'
// h(tag, props, children)
// tag: 标签名 | 组件 | 函数
// props: 属性对象
// children: 子节点数组 | 字符串 | 插槽函数
const App = defineComponent({
render() {
return h('div', { class: 'container' }, [
h('h1', { class: 'title' }, 'Hello World'),
h('p', { class: 'desc' }, '渲染函数示例')
])
}
})1.2 在 setup 中使用渲染函数
typescript
import { h, ref, defineComponent } from 'vue'
export default defineComponent({
props: {
level: { type: Number, default: 1 }
},
setup(props, { slots }) {
const tag = `h${props.level}`
return () => h(tag, { class: 'heading' }, slots.default?.())
}
})1.3 动态组件渲染
typescript
import { h, defineComponent, computed } from 'vue'
import LoadingSpinner from './LoadingSpinner.vue'
import ErrorView from './ErrorView.vue'
import DataView from './DataView.vue'
export default defineComponent({
props: {
state: { type: String, required: true }, // loading | error | success
data: { type: Object, default: null },
error: { type: Error, default: null }
},
setup(props) {
const componentMap = {
loading: LoadingSpinner,
error: ErrorView,
success: DataView
}
return () => {
const component = componentMap[props.state as keyof typeof componentMap]
if (props.state === 'success') {
return h(component, { data: props.data })
}
if (props.state === 'error') {
return h(component, { error: props.error })
}
return h(component)
}
}
})1.4 VNode 的结构
typescript
// VNode 对象结构
interface VNode {
type: string | Component | Function // 标签名或组件
props: Record<string, any> | null // 属性/事件
children: VNode[] | string | null // 子节点
key: string | number | null // Diff 的 key
el: Node | null // 真实 DOM 引用
ref: Ref | null // 模板引用
}
// 手动创建 VNode
const vnode = h('div', {
class: 'wrapper',
onClick: () => console.log('clicked'),
key: 'unique-key'
}, '内容')二、函数式组件
2.1 函数式组件基础
Vue 3 中,函数式组件就是一个返回 VNode 的普通函数:
typescript
import { h, FunctionalComponent } from 'vue'
// 类型定义
interface ButtonProps {
type?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
}
// 函数式组件
const Button: FunctionalComponent<ButtonProps> = (props, { slots, emit }) => {
return h('button', {
class: [
'btn',
`btn-${props.type || 'primary'}`,
`btn-${props.size || 'md'}`
],
disabled: props.disabled,
onClick: () => emit('click')
}, slots.default?.())
}
// 定义 props
Button.props = ['type', 'size', 'disabled']
Button.emits = ['click']
export default Button2.2 函数式组件实战
typescript
// FunctionalIcon.tsx — 轻量级图标组件
import { h, FunctionalComponent } from 'vue'
interface IconProps {
name: string
size?: number
color?: string
}
const iconPaths: Record<string, string> = {
home: 'M3 12L12 3l9 9M5 10v10h14V10',
user: 'M12 12a4 4 0 100-8 4 4 0 000 8zm0 2c-4 0-8 2-8 6v2h16v-2c0-4-4-6-8-6z',
settings: 'M12 8a4 4 0 100 8 4 4 0 000-8zM19.4 13a7.5 7.5 0 000-2l2-1.5-2-3.5-2.5 1a7.5 7.5 0 00-1.7-1l-.4-2.5H9.2l-.4 2.5a7.5 7.5 0 00-1.7 1l-2.5-1-2 3.5L2.6 11a7.5 7.5 0 000 2l-2 1.5 2 3.5 2.5-1a7.5 7.5 0 001.7 1l.4 2.5h5.6l.4-2.5a7.5 7.5 0 001.7-1l2.5 1 2-3.5-2-1.5z'
}
const FunctionalIcon: FunctionalComponent<IconProps> = (props) => {
return h('svg', {
width: props.size || 24,
height: props.size || 24,
viewBox: '0 0 24 24',
fill: 'none',
stroke: props.color || 'currentColor',
'stroke-width': 2,
'stroke-linecap': 'round',
'stroke-linejoin': 'round'
}, [
h('path', { d: iconPaths[props.name] || '' })
])
}
FunctionalIcon.props = ['name', 'size', 'color']
export default FunctionalIcon三、高阶组件(HOC)
3.1 高阶组件模式
typescript
import { h, defineComponent, ref, onMounted, Component } from 'vue'
// 高阶组件:添加数据加载能力
function withFetch<T>(WrappedComponent: Component, url: string) {
return defineComponent({
name: 'WithFetch',
setup() {
const data = ref<T | null>(null)
const loading = ref(true)
const error = ref<Error | null>(null)
const fetchData = async () => {
loading.value = true
error.value = null
try {
const response = await fetch(url)
data.value = await response.json() as T
} catch (err) {
error.value = err as Error
} finally {
loading.value = false
}
}
onMounted(fetchData)
return () => h(WrappedComponent, {
data: data.value,
loading: loading.value,
error: error.value,
onRetry: fetchData
})
}
})
}
// 使用
import UserList from './UserList.vue'
const UserListWithFetch = withFetch<User[]>(
UserList,
'/api/users'
)3.2 高阶组件:添加权限控制
typescript
import { h, defineComponent, inject, Component, Ref } from 'vue'
function withAuth(WrappedComponent: Component) {
return defineComponent({
name: 'WithAuth',
setup(props, { attrs, slots }) {
const user = inject<Ref<{ role: string } | null>>('currentUser')
return () => {
if (!user?.value) {
return h('div', { class: 'unauthorized' }, '请先登录')
}
if (user.value.role !== 'admin') {
return h('div', { class: 'forbidden' }, '权限不足')
}
return h(WrappedComponent, { ...attrs }, slots)
}
}
})
}
// 使用
import AdminPanel from './AdminPanel.vue'
const SecureAdminPanel = withAuth(AdminPanel)3.3 高阶组件:防抖/节流
typescript
import { h, defineComponent, Component } from 'vue'
function withDebounce(WrappedComponent: Component, delay = 300) {
let timer: NodeJS.Timeout
return defineComponent({
name: 'WithDebounce',
setup(props, { attrs }) {
const debouncedHandler = (...args: any[]) => {
clearTimeout(timer)
timer = setTimeout(() => {
// 调用原始事件
;(attrs.onAction as Function)?.(...args)
}, delay)
}
return () => h(WrappedComponent, {
...attrs,
onAction: debouncedHandler
})
}
})
}四、Slot 进阶技巧
4.1 作用域插槽
vue
<!-- DataTable.vue -->
<script setup lang="ts">
interface Column {
key: string
title: string
}
defineProps<{
columns: Column[]
data: Record<string, any>[]
}>()
</script>
<template>
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key">{{ col.title }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in data" :key="index">
<td v-for="col in columns" :key="col.key">
<!-- 作用域插槽:将行数据传给父组件 -->
<slot :name="col.key" :row="row" :index="index">
{{ row[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>vue
<!-- 使用 -->
<DataTable :columns="columns" :data="users">
<!-- 自定义状态列渲染 -->
<template #status="{ row }">
<span :class="row.status === 'active' ? 'text-green' : 'text-red'">
{{ row.status === 'active' ? '启用' : '禁用' }}
</span>
</template>
<!-- 自定义操作列 -->
<template #actions="{ row, index }">
<button @click="edit(row)">编辑</button>
<button @click="remove(index)">删除</button>
</template>
</DataTable>4.2 渲染函数中的插槽
typescript
import { h, defineComponent } from 'vue'
export default defineComponent({
props: {
items: { type: Array, required: true }
},
setup(props, { slots }) {
return () => h('ul', { class: 'list' },
props.items.map((item, index) =>
h('li', { key: index },
// 调用作用域插槽
slots.item?.({ item, index }) ?? h('span', String(item))
)
)
)
}
})4.3 动态插槽名
vue
<script setup>
const slots = ref(['header', 'body', 'footer'])
</script>
<template>
<div>
<template v-for="name in slots" :key="name">
<slot :name="name"></slot>
</template>
</div>
</template>五、自定义指令
5.1 指令钩子函数
typescript
import { Directive } from 'vue'
const myDirective: Directive = {
// 在绑定元素挂载前调用
created(el, binding, vnode, prevVnode) {},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding) {},
// 在元素挂载后调用
mounted(el, binding) {
console.log('指令绑定', binding.value)
},
// 在更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在更新后调用
updated(el, binding) {},
// 在卸载前调用
beforeUnmount(el, binding) {},
// 在卸载后调用
unmounted(el, binding) {}
}5.2 实用指令实战
typescript
// directives/permission.ts — 权限控制指令
import { Directive } from 'vue'
export const vPermission: Directive<HTMLElement, string> = {
mounted(el, binding) {
const userRole = localStorage.getItem('role') // 实际从 store 获取
if (userRole !== binding.value) {
el.parentNode?.removeChild(el)
}
}
}
// 使用:<button v-permission="'admin'">删除</button>typescript
// directives/copy.ts — 点击复制指令
import { Directive } from 'vue'
export const vCopy: Directive<HTMLElement, string> = {
mounted(el, binding) {
el.style.cursor = 'pointer'
el.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(binding.value)
// 显示复制成功提示
const tooltip = document.createElement('span')
tooltip.textContent = '已复制'
tooltip.style.cssText = `
position: absolute; background: #333; color: #fff;
padding: 4px 8px; border-radius: 4px; font-size: 12px;
top: -30px; left: 50%; transform: translateX(-50%);
`
el.style.position = 'relative'
el.appendChild(tooltip)
setTimeout(() => tooltip.remove(), 1500)
} catch (err) {
console.error('复制失败', err)
}
})
}
}
// 使用:<span v-copy="text">{{ text }}</span>typescript
// directives/lazy.ts — 图片懒加载指令
import { Directive } from 'vue'
export const vLazy: Directive<HTMLImageElement, string> = {
mounted(el, binding) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
el.src = binding.value
observer.unobserve(el)
}
})
},
{ rootMargin: '50px' }
)
observer.observe(el)
// 保存 observer 用于清理
el._observer = observer
},
unmounted(el) {
el._observer?.disconnect()
}
}
// 使用:<img v-lazy="imageUrl" alt="..." />typescript
// directives/debounce.ts — 防抖指令
import { Directive } from 'vue'
export const vDebounce: Directive<HTMLElement, { handler: () => void; delay: number }> = {
mounted(el, binding) {
let timer: NodeJS.Timeout
el.addEventListener('click', () => {
clearTimeout(timer)
timer = setTimeout(() => {
binding.value.handler()
}, binding.value.delay || 300)
})
}
}
// 使用:<button v-debounce="{ handler: handleClick, delay: 500 }">提交</button>5.3 注册指令
typescript
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { vPermission } from './directives/permission'
import { vCopy } from './directives/copy'
import { vLazy } from './directives/lazy'
import { vDebounce } from './directives/debounce'
const app = createApp(App)
app.directive('permission', vPermission)
app.directive('copy', vCopy)
app.directive('lazy', vLazy)
app.directive('debounce', vDebounce)
app.mount('#app')六、Provide / Inject 依赖注入
6.1 基础用法
typescript
// 父组件
import { provide, ref, InjectionKey } from 'vue'
// 定义 InjectionKey(类型安全)
export const ThemeKey: InjectionKey<{
theme: Ref<string>
toggleTheme: () => void
}> = Symbol('theme')
// 祖先组件
const theme = ref('light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide(ThemeKey, { theme, toggleTheme })typescript
// 后代组件(任意深度)
import { inject } from 'vue'
import { ThemeKey } from './parent'
const { theme, toggleTheme } = inject(ThemeKey)!
// 使用
console.log(theme.value) // 'light'
toggleTheme()6.2 默认值与工厂模式
typescript
// 提供默认值
const config = inject('config', { apiBase: '/api', timeout: 5000 })
// 工厂函数作为默认值
const user = inject('user', () => ({ name: 'Guest' }), true) // true 表示工厂函数6.3 创建可组合的 Provider
typescript
// composables/useDialog.ts
import { provide, inject, ref, readonly, InjectionKey } from 'vue'
interface DialogContext {
visible: Readonly<Ref<boolean>>
title: Readonly<Ref<string>>
open: (title?: string) => void
close: () => void
}
const DialogKey: InjectionKey<DialogContext> = Symbol('dialog')
export function provideDialog() {
const visible = ref(false)
const title = ref('')
const open = (t = '') => {
title.value = t
visible.value = true
}
const close = () => {
visible.value = false
}
const context: DialogContext = {
visible: readonly(visible),
title: readonly(title),
open,
close
}
provide(DialogKey, context)
return context
}
export function useDialog() {
const context = inject(DialogKey)
if (!context) {
throw new Error('useDialog 必须在 provideDialog 的作用域内使用')
}
return context
}七、组件设计模式
7.1 Render Props 模式
typescript
import { h, defineComponent, PropType } from 'vue'
export default defineComponent({
props: {
render: { type: Function as PropType<(data: any) => any>, required: true }
},
setup(props) {
const data = { items: [1, 2, 3], total: 3 }
return () => h('div', { class: 'container' }, props.render(data))
}
})
// 使用
// <RenderContainer :render="data => h('ul', data.items.map(i => h('li', String(i))))" />7.2 无渲染组件
typescript
// 无渲染组件:只负责逻辑,不负责渲染
import { defineComponent, ref, computed, PropType } from 'vue'
export default defineComponent({
name: 'UseCounter',
props: {
initial: { type: Number, default: 0 },
step: { type: Number, default: 1 }
},
setup(props, { slots }) {
const count = ref(props.initial)
const doubled = computed(() => count.value * 2)
const increment = () => { count.value += props.step }
const decrement = () => { count.value -= props.step }
const reset = () => { count.value = props.initial }
return () => slots.default?.({
count: count.value,
doubled: doubled.value,
increment,
decrement,
reset
})
}
})
// 使用
// <UseCounter :initial="10" v-slot="{ count, increment }">
// <button @click="increment">Count: {{ count }}</button>
// </UseCounter>7.3 组合模式
typescript
// Tab 组件系统
// TabContainer.vue
import { provide, ref, reactive, defineComponent } from 'vue'
export const TabContextKey = Symbol('tab-context')
export default defineComponent({
name: 'TabContainer',
setup(props, { slots }) {
const activeTab = ref('')
const tabs = reactive<string[]>([])
const registerTab = (id: string) => {
if (!tabs.includes(id)) {
tabs.push(id)
if (!activeTab.value) activeTab.value = id
}
}
const setActiveTab = (id: string) => {
activeTab.value = id
}
provide(TabContextKey, { activeTab, registerTab, setActiveTab })
return () => h('div', { class: 'tabs' }, slots.default?.())
}
})八、总结
- ✅ 掌握渲染函数与 h 函数的用法
- ✅ 实现函数式组件(轻量、无状态)
- ✅ 高阶组件(HOC)模式(数据加载、权限控制、防抖)
- ✅ Slot 进阶(作用域插槽、渲染函数中的插槽、动态插槽名)
- ✅ 自定义指令(权限、复制、懒加载、防抖)
- ✅ Provide/Inject 依赖注入(类型安全、可组合 Provider)
- ✅ 组件设计模式(Render Props、无渲染组件、组合模式)
高级组件设计是 Vue 3 的精华所在,掌握这些模式让你能够构建出灵活、可复用、可维护的组件库。
相关阅读: