渲染函数
渲染函数是 Vue 组件的底层实现方式,它提供了比模板更强大的编程能力。当模板的表达能力不够时,渲染函数是一个很好的选择。
基本概念
什么是渲染函数?
渲染函数是一个返回虚拟 DOM 节点的函数。Vue 的模板最终也会被编译成渲染函数。
javascript
import { h } from 'vue'
// 使用渲染函数创建组件
export default {
render() {
return h('div', { class: 'hello' }, 'Hello World!')
}
}
// 等价的模板
// <template>
// <div class="hello">Hello World!</div>
// </template>
h() 函数
h()
函数用于创建虚拟 DOM 节点(VNode)。
javascript
import { h } from 'vue'
// h(tag, props, children)
// 基本用法
h('div') // <div></div>
h('div', { id: 'app' }) // <div id="app"></div>
h('div', { id: 'app' }, 'Hello') // <div id="app">Hello</div>
// 嵌套元素
h('div', { id: 'app' }, [
h('h1', 'Title'),
h('p', 'Paragraph')
])
// <div id="app">
// <h1>Title</h1>
// <p>Paragraph</p>
// </div>
基本示例
简单的渲染函数组件
javascript
import { h, ref } from 'vue'
export default {
name: 'Counter',
setup() {
const count = ref(0)
const increment = () => {
count.value++
}
return () => h('div', { class: 'counter' }, [
h('h2', `Count: ${count.value}`),
h('button', { onClick: increment }, 'Increment')
])
}
}
使用 JSX
jsx
import { ref, defineComponent } from 'vue'
export default defineComponent({
name: 'Counter',
setup() {
const count = ref(0)
const increment = () => {
count.value++
}
return () => (
<div class="counter">
<h2>Count: {count.value}</h2>
<button onClick={increment}>Increment</button>
</div>
)
}
})
高级用法
动态组件
javascript
import { h, ref, computed } from 'vue'
import ButtonComponent from './Button.vue'
import InputComponent from './Input.vue'
import SelectComponent from './Select.vue'
export default {
name: 'DynamicForm',
props: {
fields: {
type: Array,
required: true
}
},
setup(props) {
const formData = ref({})
const componentMap = {
button: ButtonComponent,
input: InputComponent,
select: SelectComponent
}
const renderField = (field) => {
const Component = componentMap[field.type]
if (!Component) {
console.warn(`Unknown field type: ${field.type}`)
return null
}
return h(Component, {
key: field.name,
modelValue: formData.value[field.name],
'onUpdate:modelValue': (value) => {
formData.value[field.name] = value
},
...field.props
})
}
return () => h('form', { class: 'dynamic-form' }, [
...props.fields.map(renderField),
h('button', { type: 'submit' }, 'Submit')
])
}
}
条件渲染
javascript
import { h, ref } from 'vue'
export default {
name: 'ConditionalRender',
setup() {
const showContent = ref(true)
const userRole = ref('admin')
const toggleContent = () => {
showContent.value = !showContent.value
}
const renderContent = () => {
if (!showContent.value) {
return h('p', 'Content is hidden')
}
const content = []
// 基础内容
content.push(h('h2', 'Welcome!'))
// 根据用户角色渲染不同内容
if (userRole.value === 'admin') {
content.push(
h('div', { class: 'admin-panel' }, [
h('h3', 'Admin Panel'),
h('button', 'Manage Users'),
h('button', 'System Settings')
])
)
} else if (userRole.value === 'user') {
content.push(
h('div', { class: 'user-panel' }, [
h('h3', 'User Dashboard'),
h('button', 'Profile'),
h('button', 'Settings')
])
)
}
return content
}
return () => h('div', { class: 'conditional-render' }, [
h('button', { onClick: toggleContent },
showContent.value ? 'Hide Content' : 'Show Content'
),
...renderContent()
])
}
}
列表渲染
javascript
import { h, ref, computed } from 'vue'
export default {
name: 'TodoList',
setup() {
const todos = ref([
{ id: 1, text: 'Learn Vue', completed: false },
{ id: 2, text: 'Build an app', completed: false },
{ id: 3, text: 'Deploy to production', completed: true }
])
const filter = ref('all') // 'all', 'active', 'completed'
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(todo => !todo.completed)
case 'completed':
return todos.value.filter(todo => todo.completed)
default:
return todos.value
}
})
const toggleTodo = (id) => {
const todo = todos.value.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
const removeTodo = (id) => {
const index = todos.value.findIndex(t => t.id === id)
if (index > -1) {
todos.value.splice(index, 1)
}
}
const renderTodo = (todo) => {
return h('li', {
key: todo.id,
class: {
'todo-item': true,
'completed': todo.completed
}
}, [
h('input', {
type: 'checkbox',
checked: todo.completed,
onChange: () => toggleTodo(todo.id)
}),
h('span', { class: 'todo-text' }, todo.text),
h('button', {
class: 'remove-btn',
onClick: () => removeTodo(todo.id)
}, '×')
])
}
const renderFilter = (filterType, label) => {
return h('button', {
class: {
'filter-btn': true,
'active': filter.value === filterType
},
onClick: () => { filter.value = filterType }
}, label)
}
return () => h('div', { class: 'todo-app' }, [
h('h1', 'Todo List'),
// 过滤器
h('div', { class: 'filters' }, [
renderFilter('all', 'All'),
renderFilter('active', 'Active'),
renderFilter('completed', 'Completed')
]),
// 待办事项列表
h('ul', { class: 'todo-list' },
filteredTodos.value.map(renderTodo)
),
// 统计信息
h('div', { class: 'stats' }, [
h('span', `Total: ${todos.value.length}`),
h('span', `Active: ${todos.value.filter(t => !t.completed).length}`),
h('span', `Completed: ${todos.value.filter(t => t.completed).length}`)
])
])
}
}
插槽的渲染函数实现
基本插槽
javascript
import { h } from 'vue'
// 父组件
export default {
name: 'Parent',
setup() {
return () => h(Card, null, {
// 默认插槽
default: () => [
h('h3', 'Card Title'),
h('p', 'Card content goes here')
]
})
}
}
// Card 组件
const Card = {
name: 'Card',
setup(props, { slots }) {
return () => h('div', { class: 'card' }, [
h('div', { class: 'card-header' }, 'Header'),
h('div', { class: 'card-body' }, slots.default?.()),
h('div', { class: 'card-footer' }, 'Footer')
])
}
}
具名插槽
javascript
import { h } from 'vue'
// 父组件
export default {
name: 'Parent',
setup() {
return () => h(Layout, null, {
header: () => h('h1', 'Page Title'),
default: () => h('p', 'Main content'),
footer: () => h('p', 'Copyright 2023')
})
}
}
// Layout 组件
const Layout = {
name: 'Layout',
setup(props, { slots }) {
return () => h('div', { class: 'layout' }, [
h('header', slots.header?.()),
h('main', slots.default?.()),
h('footer', slots.footer?.())
])
}
}
作用域插槽
javascript
import { h, ref } from 'vue'
// 父组件
export default {
name: 'Parent',
setup() {
return () => h(DataList, {
items: [
{ id: 1, name: 'Item 1', value: 100 },
{ id: 2, name: 'Item 2', value: 200 }
]
}, {
default: ({ item, index }) => [
h('strong', `${index + 1}. ${item.name}`),
h('span', ` - Value: ${item.value}`)
]
})
}
}
// DataList 组件
const DataList = {
name: 'DataList',
props: {
items: {
type: Array,
default: () => []
}
},
setup(props, { slots }) {
return () => h('ul', { class: 'data-list' },
props.items.map((item, index) =>
h('li', { key: item.id },
slots.default?.({ item, index })
)
)
)
}
}
函数式组件
函数式组件是无状态的,它们只接收 props 并返回 VNode。
javascript
import { h } from 'vue'
// 函数式组件
const FunctionalButton = (props, { slots, emit }) => {
const handleClick = (event) => {
emit('click', event)
}
return h('button', {
class: {
'btn': true,
[`btn-${props.type}`]: props.type,
'btn-disabled': props.disabled
},
disabled: props.disabled,
onClick: handleClick
}, slots.default?.())
}
// 定义 props
FunctionalButton.props = {
type: {
type: String,
default: 'default'
},
disabled: {
type: Boolean,
default: false
}
}
// 定义 emits
FunctionalButton.emits = ['click']
export default FunctionalButton
高阶组件 (HOC)
javascript
import { h, defineComponent } from 'vue'
// 创建一个高阶组件,为组件添加加载状态
function withLoading(WrappedComponent) {
return defineComponent({
name: `WithLoading${WrappedComponent.name}`,
props: {
loading: {
type: Boolean,
default: false
}
},
setup(props, { slots, attrs }) {
return () => {
if (props.loading) {
return h('div', { class: 'loading-wrapper' }, [
h('div', { class: 'spinner' }),
h('p', 'Loading...')
])
}
return h(WrappedComponent, {
...attrs,
...props
}, slots)
}
}
})
}
// 使用高阶组件
const MyComponent = {
name: 'MyComponent',
setup() {
return () => h('div', 'My Component Content')
}
}
const MyComponentWithLoading = withLoading(MyComponent)
// 在父组件中使用
export default {
setup() {
const loading = ref(true)
setTimeout(() => {
loading.value = false
}, 2000)
return () => h(MyComponentWithLoading, {
loading: loading.value
})
}
}
渲染函数 vs 模板
何时使用渲染函数
javascript
// ✅ 适合使用渲染函数的场景:
// 1. 动态组件名
const DynamicComponent = {
props: ['tag'],
setup(props, { slots }) {
return () => h(props.tag || 'div', slots.default?.())
}
}
// 2. 复杂的条件逻辑
const ComplexConditional = {
props: ['type', 'level', 'variant'],
setup(props) {
return () => {
let tag = 'div'
let className = 'base'
if (props.type === 'heading') {
tag = `h${props.level || 1}`
className = `heading heading-${props.level}`
} else if (props.type === 'button') {
tag = 'button'
className = `btn btn-${props.variant || 'default'}`
}
return h(tag, { class: className }, 'Content')
}
}
}
// 3. 需要编程式创建大量重复结构
const Table = {
props: ['columns', 'data'],
setup(props) {
return () => h('table', [
h('thead',
h('tr', props.columns.map(col =>
h('th', { key: col.key }, col.title)
))
),
h('tbody',
props.data.map((row, index) =>
h('tr', { key: index },
props.columns.map(col =>
h('td', { key: col.key }, row[col.key])
)
)
)
)
])
}
}
何时使用模板
vue
<!-- ✅ 适合使用模板的场景: -->
<!-- 1. 静态结构 -->
<template>
<div class="card">
<header class="card-header">
<h3>{{ title }}</h3>
</header>
<div class="card-body">
<slot></slot>
</div>
</div>
</template>
<!-- 2. 简单的条件和循环 -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<span v-if="item.completed" class="completed">✓</span>
{{ item.text }}
</li>
</ul>
</template>
<!-- 3. 表单和用户交互 -->
<template>
<form @submit.prevent="handleSubmit">
<input v-model="form.name" placeholder="Name" required>
<input v-model="form.email" type="email" placeholder="Email" required>
<button type="submit" :disabled="!isValid">Submit</button>
</form>
</template>
性能优化
避免在渲染函数中创建新对象
javascript
// ❌ 每次渲染都创建新对象
const BadComponent = {
setup() {
return () => h('div', {
style: { color: 'red' }, // 每次都是新对象
class: { active: true } // 每次都是新对象
})
}
}
// ✅ 复用对象
const GoodComponent = {
setup() {
const style = { color: 'red' }
const className = { active: true }
return () => h('div', {
style,
class: className
})
}
}
使用 key 优化列表渲染
javascript
const OptimizedList = {
props: ['items'],
setup(props) {
return () => h('ul',
props.items.map(item =>
h('li', {
key: item.id // 重要:提供稳定的 key
}, item.text)
)
)
}
}
使用 memo 缓存组件
javascript
import { h, memo } from 'vue'
// 使用 memo 包装组件以避免不必要的重新渲染
const ExpensiveComponent = memo({
props: ['data'],
setup(props) {
return () => {
// 昂贵的计算...
const processedData = expensiveCalculation(props.data)
return h('div', processedData)
}
}
})
调试渲染函数
使用开发工具
javascript
import { h, getCurrentInstance } from 'vue'
const DebugComponent = {
setup() {
const instance = getCurrentInstance()
return () => {
// 在开发环境中添加调试信息
if (process.env.NODE_ENV === 'development') {
console.log('Rendering component:', instance?.type.name)
}
return h('div', 'Debug Component')
}
}
}
渲染函数的类型检查
typescript
import { h, defineComponent, PropType } from 'vue'
interface User {
id: number
name: string
email: string
}
export default defineComponent({
name: 'UserCard',
props: {
user: {
type: Object as PropType<User>,
required: true
},
showEmail: {
type: Boolean,
default: false
}
},
setup(props) {
return () => h('div', { class: 'user-card' }, [
h('h3', props.user.name),
props.showEmail && h('p', props.user.email)
].filter(Boolean))
}
})
最佳实践
1. 保持渲染函数简洁
javascript
// ❌ 复杂的渲染函数
const ComplexComponent = {
setup() {
return () => {
// 大量复杂逻辑...
const result = []
// 50+ 行代码...
return h('div', result)
}
}
}
// ✅ 拆分为小函数
const CleanComponent = {
setup() {
const renderHeader = () => h('header', 'Header')
const renderContent = () => h('main', 'Content')
const renderFooter = () => h('footer', 'Footer')
return () => h('div', [
renderHeader(),
renderContent(),
renderFooter()
])
}
}
2. 合理使用 JSX
jsx
// 对于复杂的 UI,JSX 更易读
export default defineComponent({
setup() {
const items = ref([])
return () => (
<div class="complex-ui">
<header>
<h1>Title</h1>
<nav>
<a href="#">Home</a>
<a href="#">About</a>
</nav>
</header>
<main>
{items.value.map(item => (
<div key={item.id} class="item">
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
))}
</main>
</div>
)
}
})
3. 类型安全
typescript
import { h, VNode } from 'vue'
// 为渲染函数添加类型注解
function createButton(
text: string,
onClick: () => void,
type: 'primary' | 'secondary' = 'primary'
): VNode {
return h('button', {
class: `btn btn-${type}`,
onClick
}, text)
}
下一步
渲染函数为 Vue 开发提供了强大的编程能力,掌握它将让你能够构建更加灵活和动态的组件!