跳转到内容

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

Vue 3 高级组件设计与渲染函数

当模板语法无法满足复杂需求时,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 Button

2.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 的精华所在,掌握这些模式让你能够构建出灵活、可复用、可维护的组件库。


相关阅读: