Skip to content

渲染函数

渲染函数是 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 开发提供了强大的编程能力,掌握它将让你能够构建更加灵活和动态的组件!

基于 Vue.js 官方文档构建的学习宝典