Vue:useSlots 和 useAttrs 深度解析

Vue3 Composition API中的useSlotsuseAttrs是处理组件插槽和属性的关键工具。


useSlots用于访问父组件传递的插槽内容,支持检查插槽存在性、动态处理插槽内容及类型安全;


useAttrs用于获取非props属性,可实现属性继承控制、过滤合并及手动绑定。


两者结合可创建高阶组件、表单包装器等灵活组件,但需注意性能优化和类型安全。


最佳实践包括:

  • 明确属性继承策略
  • 分离UI/功能属性
  • 智能处理插槽内容
  • 配合TypeScript实现类型安全

useSlotsuseAttrs 深度解析

这两个是 Vue 3 Composition API 中的实用工具函数,专门用于在 <script setup> 中访问插槽非 props 属性


1. useSlots - 访问插槽内容

基本概念

插槽(Slots)允许父组件向子组件传递模板内容。

基本用法

vue

javascript 复制代码
<!-- ChildComponent.vue -->
<script setup>
import { useSlots } from 'vue'

// 获取插槽对象
const slots = useSlots()

// 检查插槽是否存在
const hasDefaultSlot = !!slots.default
const hasHeaderSlot = !!slots.header
const hasFooterSlot = !!slots.footer

// 访问插槽内容
console.log(slots.default) // 默认插槽的 VNode 数组
console.log(slots.header)  // 具名插槽的 VNode 数组
</script>

<template>
  <div class="container">
    <!-- 条件渲染具名插槽 -->
    <header v-if="hasHeaderSlot">
      <slot name="header"></slot>
    </header>
    
    <!-- 默认插槽 -->
    <main>
      <slot></slot>
    </main>
    
    <!-- 带作用域的插槽 -->
    <footer v-if="hasFooterSlot">
      <slot name="footer" :data="footerData"></slot>
    </footer>
  </div>
</template>

父组件使用示例

vue

javascript 复制代码
<!-- ParentComponent.vue -->
<template>
  <ChildComponent>
    <!-- 默认插槽内容 -->
    <p>这是默认插槽内容</p>
    
    <!-- 具名插槽 -->
    <template #header>
      <h1>页面标题</h1>
    </template>
    
    <!-- 作用域插槽 -->
    <template #footer="{ data }">
      <p>页脚:{{ data.message }}</p>
    </template>
  </ChildComponent>
</template>

高级用法:动态插槽

vue

javascript 复制代码
<!-- DynamicSlotComponent.vue -->
<script setup>
import { useSlots, computed } from 'vue'

const slots = useSlots()

// 动态处理插槽内容
const processedSlots = computed(() => {
  const result = {}
  
  // 遍历所有插槽
  for (const [name, slotFn] of Object.entries(slots)) {
    if (slotFn) {
      // 处理插槽内容(例如添加包装)
      result[name] = () => {
        const vnodes = slotFn()
        return vnodes.map(vnode => {
          // 为每个节点添加特定类名
          return h('div', { class: 'slot-wrapper' }, [vnode])
        })
      }
    }
  }
  
  return result
})
</script>

<template>
  <!-- 动态渲染处理后的插槽 -->
  <slot v-if="processedSlots.default" :name="processedSlots.default"></slot>
  
  <!-- 或者使用渲染函数 -->
  <component 
    v-for="(slotFn, name) in processedSlots" 
    :key="name" 
    :is="slotFn"
  />
</template>

插槽类型检查(TypeScript)

vue

javascript 复制代码
<script setup lang="ts">
import { useSlots, Slots } from 'vue'

// 定义插槽类型
interface ComponentSlots {
  default?: () => any
  header?: (props: { title: string }) => any
  footer?: (props: { year: number }) => any
}

// 类型断言
const slots = useSlots() as unknown as ComponentSlots

// 现在有类型提示
if (slots.header) {
  // slots.header() 需要传递正确的 props
  const headerContent = slots.header({ title: 'Hello' })
}
</script>

2. useAttrs - 访问非 props 属性

基本概念

attrs 包含所有传递给组件的非 props 属性(class、style、自定义属性、事件监听器等)。

基本用法

vue

javascript 复制代码
<!-- ButtonComponent.vue -->
<script setup>
import { useAttrs } from 'vue'

// 获取属性对象
const attrs = useAttrs()

console.log(attrs)
// 可能包含:
// {
//   class: 'btn-primary',
//   style: { color: 'red' },
//   id: 'submit-btn',
//   'data-test': 'button',
//   onClick: fn
// }
</script>

<template>
  <!-- 手动绑定所有属性 -->
  <button v-bind="attrs">
    <slot></slot>
  </button>
</template>

属性继承控制

默认情况下,Vue 会自动继承非 props 属性到根元素。可以通过选项关闭:

vue

javascript 复制代码
<script>
// 关闭自动属性继承
export default {
  inheritAttrs: false
}
</script>

<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()

// 现在需要手动处理属性绑定
</script>

<template>
  <div class="wrapper">
    <!-- 只绑定特定属性到内部元素 -->
    <button :class="attrs.class" :style="attrs.style">
      <slot></slot>
    </button>
    
    <!-- 其他属性绑定到其他地方 -->
    <span v-bind="filteredAttrs"></span>
  </div>
</template>

属性过滤和处理

vue

javascript 复制代码
<script setup>
import { useAttrs, computed } from 'vue'

const attrs = useAttrs()

// 1. 分离 class 和 style
const { class: className, style, ...otherAttrs } = attrs

// 2. 计算属性过滤
const buttonAttrs = computed(() => {
  const result = { ...otherAttrs }
  
  // 移除特定属性
  delete result['data-internal']
  
  // 添加额外属性
  result['data-component'] = 'custom-button'
  
  return result
})

// 3. 合并 class
const mergedClass = computed(() => {
  const baseClass = 'btn'
  return [baseClass, className].filter(Boolean).join(' ')
})

// 4. 监听属性变化
import { watchEffect } from 'vue'
watchEffect(() => {
  console.log('属性变化:', attrs)
})
</script>

<template>
  <button 
    :class="mergedClass" 
    :style="style"
    v-bind="buttonAttrs"
  >
    <slot></slot>
  </button>
</template>

useSlotsuseAttrs 的实际应用场景

场景1:高阶组件/包装组件

vue

javascript 复制代码
<!-- WithLoading.vue - 加载状态包装器 -->
<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()

const props = defineProps({
  loading: Boolean,
  loadingText: {
    type: String,
    default: '加载中...'
  }
})
</script>

<template>
  <div class="with-loading" v-bind="attrs">
    <div v-if="props.loading" class="loading-overlay">
      {{ props.loadingText }}
    </div>
    
    <!-- 透传所有插槽 -->
    <slot v-if="slots.default" name="default"></slot>
    <slot v-if="slots.header" name="header"></slot>
    <slot v-if="slots.footer" name="footer"></slot>
  </div>
</template>

场景2:表单字段包装器

vue

javascript 复制代码
<!-- FormField.vue -->
<script setup>
import { useAttrs, computed } from 'vue'

const attrs = useAttrs()
const props = defineProps({
  label: String,
  error: String
})

// 提取输入框相关属性
const inputAttrs = computed(() => {
  const { class: _, style: __, label, error, ...inputProps } = attrs
  return inputProps
})

// 提取容器属性
const wrapperAttrs = computed(() => {
  const { class: wrapperClass, style: wrapperStyle } = attrs
  return { class: wrapperClass, style: wrapperStyle }
})
</script>

<template>
  <div class="form-field" v-bind="wrapperAttrs">
    <label v-if="props.label">{{ props.label }}</label>
    
    <!-- 绑定所有输入相关属性到 input -->
    <input v-bind="inputAttrs" />
    
    <div v-if="props.error" class="error-message">
      {{ props.error }}
    </div>
    
    <!-- 其他插槽内容 -->
    <slot></slot>
  </div>
</template>

场景3:组件透传

vue

javascript 复制代码
<!-- TransparentWrapper.vue -->
<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()

// 不定义任何 props,完全透传
</script>

<template>
  <!-- 透传到第三方组件或原生元素 -->
  <el-button v-bind="attrs">
    <!-- 透传所有插槽 -->
    <template v-for="(_, name) in slots" #[name]="slotProps">
      <slot :name="name" v-bind="slotProps"></slot>
    </template>
  </el-button>
</template>

TypeScript 高级用法

完整的类型安全示例

vue

TypeScript 复制代码
<script setup lang="ts">
import { useSlots, useAttrs, Slots, VNode } from 'vue'

// 1. 定义插槽类型
interface MySlots {
  default?: () => VNode[]
  title?: (props: { size: 'sm' | 'lg' }) => VNode[]
  actions?: () => VNode[]
}

// 2. 定义属性类型(排除已定义的 props)
interface MyAttrs {
  class?: string
  style?: Record<string, string | number>
  onClick?: (event: MouseEvent) => void
  [key: `data-${string}`]: string
}

// 3. 类型断言
const slots = useSlots() as unknown as MySlots
const attrs = useAttrs() as unknown as MyAttrs

// 4. 类型安全的处理函数
const renderTitleSlot = () => {
  if (slots.title) {
    return slots.title({ size: 'lg' })
  }
  return null
}

// 5. 属性类型守卫
const handleClick = (event: MouseEvent) => {
  if (attrs.onClick) {
    attrs.onClick(event)
  }
}
</script>

泛型组件包装器

vue

javascript 复制代码
<!-- GenericWrapper.vue -->
<script setup lang="ts" generic="T extends Record<string, any>">
import { useSlots, useAttrs, Slots } from 'vue'

const props = defineProps<{
  data: T
  renderItem?: (item: T) => any
}>()

const slots = useSlots()
const attrs = useAttrs()

// 泛型类型在插槽中的使用
interface ScopedSlots {
  default?: (props: { item: T; index: number }) => any
  header?: () => any
}

const typedSlots = slots as unknown as ScopedSlots
</script>

<template>
  <div v-bind="attrs">
    <slot name="header"></slot>
    
    <!-- 作用域插槽传递泛型数据 -->
    <slot 
      v-if="typedSlots.default" 
      :item="props.data" 
      :index="0"
    ></slot>
    
    <!-- 或者使用渲染函数 -->
    <template v-else-if="props.renderItem">
      {{ props.renderItem(props.data) }}
    </template>
  </div>
</template>

常见问题与解决方案

Q1: useSlots$slots 的区别?

javascript 复制代码
// Options API 中使用 $slots
export default {
  mounted() {
    console.log(this.$slots)  // Options API 方式
  }
}

// Composition API 中使用 useSlots
import { useSlots } from 'vue'
const slots = useSlots()  // Composition API 方式

// 两者访问的是相同的内容,只是 API 不同

Q2: 如何判断插槽是否为空?

javascript 复制代码
const slots = useSlots()

// ❌ 错误:直接判断布尔值
if (slots.default) { /* 可能不准确 */ }

// ✅ 正确:判断函数调用结果
if (slots.default && slots.default().length > 0) {
  // 有实际内容
}

// ✅ 更好的方法:封装工具函数
const hasSlotContent = (name = 'default') => {
  const slot = slots[name]
  return slot && slot().length > 0
}

Q3: 属性继承冲突怎么办?

vue

javascript 复制代码
<script setup>
import { useAttrs, computed } from 'vue'

const attrs = useAttrs()

// 处理 class 合并
const mergedClass = computed(() => {
  const baseClass = 'my-component'
  const attrClass = attrs.class || ''
  
  // 合并多个 class
  return `${baseClass} ${attrClass}`.trim()
})

// 处理 style 合并
const mergedStyle = computed(() => {
  const baseStyle = { color: 'blue' }
  const attrStyle = attrs.style || {}
  
  return { ...baseStyle, ...attrStyle }
})
</script>

Q4: 动态插槽名如何处理?

vue

javascript 复制代码
<script setup>
import { useSlots } from 'vue'

const slots = useSlots()
const slotName = ref('content')

// 动态访问插槽
const dynamicSlot = computed(() => {
  return slots[slotName.value]
})

// 或者使用渲染函数
const renderDynamicSlot = (name) => {
  const slot = slots[name]
  if (slot) {
    return h('div', slot())
  }
  return null
}
</script>

最佳实践总结

useSlots 最佳实践:

  1. 检查插槽存在性 :使用 slots.name?.() 而不是 slots.name

  2. 避免过度使用:只在需要动态处理插槽时使用

  3. 类型安全:为 TypeScript 项目定义插槽类型

  4. 性能考虑:避免在渲染函数中频繁调用 slot 函数

useAttrs 最佳实践:

  1. 明确继承策略 :使用 inheritAttrs: false 明确控制

  2. 属性分离:分离 UI 属性和功能属性

  3. 避免属性冲突:处理 class、style 合并

  4. 安全性:过滤敏感或不必要的属性


两者结合的最佳模式:

vue

javascript 复制代码
<!-- SmartWrapper.vue -->
<script setup>
import { useSlots, useAttrs, computed } from 'vue'

const slots = useSlots()
const attrs = useAttrs()

// 智能属性处理
const processedAttrs = computed(() => {
  const { 
    class: userClass, 
    style: userStyle,
    ...restAttrs 
  } = attrs
  
  return {
    class: `wrapper ${userClass || ''}`.trim(),
    style: { 
      position: 'relative', 
      ...(userStyle || {}) 
    },
    ...restAttrs
  }
})

// 插槽内容增强
const enhancedSlots = computed(() => {
  const result = {}
  
  Object.keys(slots).forEach(name => {
    if (slots[name]) {
      // 为每个插槽内容添加包装
      result[name] = () => {
        const content = slots[name]()
        return content.map(vnode => ({
          ...vnode,
          props: {
            ...vnode.props,
            'data-slot': name
          }
        }))
      }
    }
  })
  
  return result
})
</script>

<template>
  <div v-bind="processedAttrs">
    <template v-for="(slotFn, name) in enhancedSlots" :key="name">
      <slot :name="name" v-if="slotFn"></slot>
    </template>
  </div>
</template>

useSlotsuseAttrs 提供了在 Composition API 中访问组件"外部输入"的能力,它们是创建灵活、可复用组件的重要工具。正确使用它们可以大大提高组件的适应性和开发效率。

相关推荐
五颜六色的黑2 小时前
vue3+elementPlus实现循环列表内容超出时展开收起功能
前端·javascript·vue.js
JIngJaneIL4 小时前
基于java+ vue医院管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
计算机学姐4 小时前
基于SpringBoot的高校论坛系统【2026最新】
java·vue.js·spring boot·后端·spring·java-ee·tomcat
JIngJaneIL4 小时前
基于java + vue校园跑腿便利平台系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
越努力越幸运5086 小时前
vue学习二:
javascript·vue.js·学习
码途进化论7 小时前
Vue3 防重复点击指令 - clickOnce
前端·javascript·vue.js
幽络源小助理8 小时前
SpringBoot+Vue攀枝花水果在线销售系统源码 | Java项目免费下载 – 幽络源
java·vue.js·spring boot
雲墨款哥8 小时前
从一行好奇的代码说起:Vue怎么没有React的props.children
前端·vue.js·react.js
计算机学姐8 小时前
基于SpringBoot的演出购票系统【2026最新】
java·vue.js·spring boot·后端·spring·tomcat·intellij-idea