Vue3 组合式 API 核心宏详解:defineProps、defineEmits、defineExpose

Vue3 组合式 API 核心宏详解:defineProps、defineEmits、defineExpose

在 Vue3 的 Composition API 中,definePropsdefineEmitsdefineExpose 是三个非常重要的编译器宏,它们为组件开发提供了简洁而强大的功能。下面我将详细解释它们的含义、用法并提供完整示例。

核心概念解析

1. defineProps

  • 作用:声明组件接收的 props(属性)
  • 功能:定义组件可以从父组件接收哪些数据
  • 特点
    • 仅在 <script setup> 中可用
    • 无需导入,由编译器自动处理
    • 支持 TypeScript 类型声明
  • 替代选项式 APIprops 选项

2. defineEmits

  • 作用:声明组件可以触发的事件
  • 功能:定义组件可以向父组件发送哪些事件
  • 特点
    • 仅在 <script setup> 中可用
    • 支持完整类型标注
    • 返回一个 emit 函数用于触发事件
  • 替代选项式 APIemits 选项

3. defineExpose

  • 作用:暴露组件的公共属性/方法
  • 功能:明确指定哪些组件内部内容可以被父组件通过 ref 访问
  • 特点
    • 仅在 <script setup> 中可用
    • 默认情况下 <script setup> 的组件是封闭的
    • 用于定义组件公共 API
  • 替代选项式 APIexpose 选项

完整示例:用户资料卡片组件

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue3 组合式 API 宏详解</title>
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    }
    body {
      background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
      color: white;
      min-height: 100vh;
      padding: 2rem;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    .container {
      max-width: 1000px;
      background: rgba(255, 255, 255, 0.08);
      border-radius: 15px;
      padding: 2rem;
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
      backdrop-filter: blur(10px);
      border: 1px solid rgba(255, 255, 255, 0.1);
    }
    header {
      text-align: center;
      margin-bottom: 2rem;
    }
    h1 {
      font-size: 2.5rem;
      margin-bottom: 1rem;
      color: #61dafb;
    }
    .subtitle {
      font-size: 1.2rem;
      opacity: 0.8;
      margin-bottom: 2rem;
    }
    .content {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 2rem;
    }
    .panel {
      background: rgba(0, 0, 0, 0.2);
      border-radius: 10px;
      padding: 1.5rem;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }
    h2 {
      font-size: 1.8rem;
      margin-bottom: 1rem;
      color: #38ef7d;
      text-align: center;
    }
    .card {
      background: rgba(255, 255, 255, 0.1);
      border-radius: 10px;
      padding: 1.5rem;
      margin: 1rem 0;
    }
    .profile {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 1rem;
    }
    .avatar {
      width: 120px;
      height: 120px;
      border-radius: 50%;
      background: linear-gradient(135deg, #3494e6, #ec6ead);
      display: flex;
      justify-content: center;
      align-items: center;
      font-size: 3rem;
    }
    .user-info {
      text-align: center;
    }
    .user-name {
      font-size: 1.5rem;
      font-weight: bold;
      margin-bottom: 0.5rem;
    }
    .user-email {
      opacity: 0.8;
      margin-bottom: 1rem;
    }
    .stats {
      display: flex;
      justify-content: space-around;
      width: 100%;
      margin: 1rem 0;
    }
    .stat {
      text-align: center;
    }
    .stat-value {
      font-size: 1.5rem;
      font-weight: bold;
      color: #f8b400;
    }
    .stat-label {
      font-size: 0.9rem;
      opacity: 0.8;
    }
    .actions {
      display: flex;
      gap: 1rem;
      margin-top: 1rem;
      flex-wrap: wrap;
    }
    button {
      background: linear-gradient(135deg, #3494e6, #ec6ead);
      color: white;
      border: none;
      padding: 0.8rem 1.5rem;
      border-radius: 50px;
      cursor: pointer;
      font-size: 1rem;
      font-weight: bold;
      transition: all 0.3s ease;
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
      flex: 1;
      min-width: 120px;
    }
    button:hover {
      transform: translateY(-3px);
      box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
    }
    .code-block {
      background: rgba(0, 0, 0, 0.3);
      padding: 1rem;
      border-radius: 8px;
      font-family: 'Fira Code', monospace;
      font-size: 0.9rem;
      overflow-x: auto;
      margin: 1rem 0;
    }
    .explanation {
      margin-top: 1.5rem;
      padding: 1.5rem;
      background: rgba(0, 0, 0, 0.2);
      border-radius: 10px;
      border-left: 4px solid #61dafb;
    }
    h3 {
      font-size: 1.5rem;
      margin-bottom: 1rem;
      color: #ff7e5f;
    }
    ul {
      padding-left: 1.5rem;
      margin: 1rem 0;
    }
    li {
      margin-bottom: 0.5rem;
      line-height: 1.6;
    }
    .usage {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 1rem;
      margin-top: 1.5rem;
    }
    .usage-card {
      background: rgba(255, 255, 255, 0.1);
      border-radius: 8px;
      padding: 1rem;
    }
    .usage-title {
      font-weight: bold;
      margin-bottom: 0.5rem;
      color: #38ef7d;
    }
    @media (max-width: 768px) {
      .content {
        grid-template-columns: 1fr;
      }
      .usage {
        grid-template-columns: 1fr;
      }
    }
  </style>
</head>
<body>
  <div id="app">
    <div class="container">
      <header>
        <h1>Vue3 组合式 API 核心宏</h1>
        <p class="subtitle">defineProps, defineEmits, defineExpose 详解</p>
      </header>
      
      <div class="content">
        <!-- 父组件 -->
        <div class="panel">
          <h2>父组件 (App.vue)</h2>
          
          <div class="card">
            <div class="code-block">
// 父组件模板
&lt;UserProfile 
  ref="profileRef"
  :user="userData"
  :show-stats="true"
  @update-name="handleNameUpdate"
  @follow="handleFollow"
/>

// 父组件逻辑
import { ref } from 'vue'
import UserProfile from './UserProfile.vue'

const profileRef = ref(null)
const userData = ref({
  name: '张三',
  email: '[email protected]',
  posts: 42,
  followers: 1200,
  following: 350
})

function handleNameUpdate(newName) {
  userData.value.name = newName
}

function handleFollow() {
  userData.value.followers++
}

function resetStats() {
  profileRef.value.resetStats()
}
            </div>
            
            <div class="actions">
              <button @click="resetStats">调用子组件方法 (resetStats)</button>
              <button @click="showChildData">查看子组件数据</button>
            </div>
          </div>
          
          <div class="card">
            <h3>当前用户数据</h3>
            <div class="code-block">
{{ JSON.stringify(parentUser, null, 2) }}
            </div>
          </div>
        </div>
        
        <!-- 子组件 -->
        <div class="panel">
          <h2>子组件 (UserProfile.vue)</h2>
          
          <div class="card">
            <div class="profile">
              <div class="avatar">
                {{ userInitials }}
              </div>
              <div class="user-info">
                <div class="user-name">{{ props.user.name }}</div>
                <div class="user-email">{{ props.user.email }}</div>
              </div>
              
              <div v-if="props.showStats" class="stats">
                <div class="stat">
                  <div class="stat-value">{{ props.user.posts }}</div>
                  <div class="stat-label">文章</div>
                </div>
                <div class="stat">
                  <div class="stat-value">{{ props.user.followers }}</div>
                  <div class="stat-label">粉丝</div>
                </div>
                <div class="stat">
                  <div class="stat-value">{{ props.user.following }}</div>
                  <div class="stat-label">关注</div>
                </div>
              </div>
              
              <div class="actions">
                <button @click="updateName">更新名称</button>
                <button @click="followUser">关注用户</button>
              </div>
            </div>
          </div>
          
          <div class="card">
            <div class="code-block">
// 子组件逻辑
import { computed } from 'vue'

// 1. 使用 defineProps 声明接收的属性
const props = defineProps({
  user: {
    type: Object,
    required: true
  },
  showStats: {
    type: Boolean,
    default: true
  }
})

// 2. 使用 defineEmits 声明可触发的事件
const emit = defineEmits(['update-name', 'follow'])

// 计算属性
const userInitials = computed(() => 
  props.user.name.split(' ').map(n => n[0]).join('')
)

// 3. 使用 defineExpose 暴露公共方法
defineExpose({
  resetStats: () => {
    props.user.posts = 0
    props.user.followers = 0
    props.user.following = 0
  }
})

// 组件方法
function updateName() {
  const newName = prompt('请输入新名称', props.user.name)
  if (newName) {
    emit('update-name', newName)
  }
}

function followUser() {
  emit('follow')
}
            </div>
          </div>
        </div>
      </div>
      
      <div class="explanation">
        <h3>核心宏详解</h3>
        
        <div class="usage">
          <div class="usage-card">
            <div class="usage-title">defineProps</div>
            <p>用于声明组件接收的属性:</p>
            <ul>
              <li>类型验证</li>
              <li>默认值</li>
              <li>必需性检查</li>
            </ul>
            <div class="code-block">
const props = defineProps({
  title: String,
  count: {
    type: Number,
    default: 0,
    required: true
  }
})
            </div>
          </div>
          
          <div class="usage-card">
            <div class="usage-title">defineEmits</div>
            <p>用于声明组件触发的事件:</p>
            <ul>
              <li>事件名声明</li>
              <li>事件参数类型</li>
              <li>事件验证</li>
            </ul>
            <div class="code-block">
const emit = defineEmits({
  'update-name': (newName) => {
    return newName.length > 0
  },
  'follow': null // 无需验证
})
            </div>
          </div>
          
          <div class="usage-card">
            <div class="usage-title">defineExpose</div>
            <p>用于暴露公共API:</p>
            <ul>
              <li>暴露方法</li>
              <li>暴露属性</li>
              <li>控制访问权限</li>
            </ul>
            <div class="code-block">
const internalData = ref('私有数据')

defineExpose({
  publicMethod() {
    console.log('公共方法')
  },
  publicData: '公共数据'
})
            </div>
          </div>
        </div>
        
        <div class="important-note">
          <h3>最佳实践</h3>
          <ul>
            <li>始终为 props 定义类型验证,提高代码健壮性</li>
            <li>使用 defineEmits 明确声明事件,提供更好的类型支持</li>
            <li>谨慎使用 defineExpose,只暴露必要的公共 API</li>
            <li>对于复杂组件,使用 TypeScript 接口定义 props 和 emits 类型</li>
            <li>避免直接修改 props 的值,应该通过触发事件让父组件修改</li>
          </ul>
        </div>
      </div>
    </div>
  </div>

  <script>
    const { createApp, ref, computed } = Vue;
    
    // 创建Vue应用
    const app = createApp({
      setup() {
        // 父组件数据
        const parentUser = ref({
          name: '张三',
          email: '[email protected]',
          posts: 42,
          followers: 1200,
          following: 350
        });
        
        // 引用子组件
        const profileRef = ref(null);
        
        // 处理名称更新
        const handleNameUpdate = (newName) => {
          parentUser.value.name = newName;
        };
        
        // 处理关注事件
        const handleFollow = () => {
          parentUser.value.followers++;
          alert('关注成功!');
        };
        
        // 重置统计数据
        const resetStats = () => {
          if (profileRef.value && profileRef.value.resetStats) {
            profileRef.value.resetStats();
            alert('统计数据已重置!');
          }
        };
        
        // 查看子组件数据
        const showChildData = () => {
          if (profileRef.value) {
            console.log('子组件暴露的内容:', profileRef.value);
            alert('请在控制台查看子组件暴露的内容');
          }
        };
        
        return {
          parentUser,
          profileRef,
          handleNameUpdate,
          handleFollow,
          resetStats,
          showChildData
        };
      }
    });
    
    // 注册子组件
    app.component('UserProfile', {
      // 使用 <script setup> 语法等效代码
      props: {
        user: {
          type: Object,
          required: true
        },
        showStats: {
          type: Boolean,
          default: true
        }
      },
      
      emits: ['update-name', 'follow'],
      
      setup(props, { emit, expose }) {
        // 计算用户首字母
        const userInitials = computed(() => 
          props.user.name.split(' ').map(n => n[0]).join('')
        );
        
        // 更新名称方法
        const updateName = () => {
          const newName = prompt('请输入新名称', props.user.name);
          if (newName) {
            emit('update-name', newName);
          }
        };
        
        // 关注用户方法
        const followUser = () => {
          emit('follow');
        };
        
        // 重置统计方法
        const resetStats = () => {
          props.user.posts = 0;
          props.user.followers = 0;
          props.user.following = 0;
        };
        
        // 暴露公共API
        expose({
          resetStats
        });
        
        return {
          props,
          userInitials,
          updateName,
          followUser
        };
      },
      
      template: `
        <div class="profile">
          <div class="avatar">
            {{ userInitials }}
          </div>
          <div class="user-info">
            <div class="user-name">{{ props.user.name }}</div>
            <div class="user-email">{{ props.user.email }}</div>
          </div>
          
          <div v-if="props.showStats" class="stats">
            <div class="stat">
              <div class="stat-value">{{ props.user.posts }}</div>
              <div class="stat-label">文章</div>
            </div>
            <div class="stat">
              <div class="stat-value">{{ props.user.followers }}</div>
              <div class="stat-label">粉丝</div>
            </div>
            <div class="stat">
              <div class="stat-value">{{ props.user.following }}</div>
              <div class="stat-label">关注</div>
            </div>
          </div>
          
          <div class="actions">
            <button @click="updateName">更新名称</button>
            <button @click="followUser">关注用户</button>
          </div>
        </div>
      `
    });
    
    // 挂载应用
    app.mount('#app');
  </script>
</body>
</html>

核心宏详细说明

1. defineProps - 属性声明

用法:

javascript 复制代码
const props = defineProps({
  // 基础类型检查
  title: String,
  
  // 多个可能的类型
  content: [String, Number],
  
  // 必填且为数字
  count: {
    type: Number,
    required: true
  },
  
  // 带默认值的对象
  user: {
    type: Object,
    default: () => ({ name: 'Guest' })
  },
  
  // 自定义验证函数
  age: {
    validator(value) {
      return value >= 18 && value <= 100;
    }
  }
})

TypeScript 用法:

typescript 复制代码
interface User {
  name: string
  age: number
}

const props = defineProps<{
  title: string
  count?: number  // 可选
  user: User      // 必填
}>()

2. defineEmits - 事件声明

用法:

javascript 复制代码
const emit = defineEmits({
  // 无验证
  'change': null,
  
  // 有验证
  'update': (payload) => {
    return typeof payload === 'string'
  },
  
  // 多个参数验证
  'submit': (email, password) => {
    return email.includes('@') && password.length >= 8
  }
})

// 触发事件
emit('update', 'new value')

TypeScript 用法:

typescript 复制代码
const emit = defineEmits<{
  (e: 'change'): void
  (e: 'update', value: string): void
  (e: 'submit', email: string, password: string): void
}>()

3. defineExpose - 暴露公共API

用法:

javascript 复制代码
import { ref } from 'vue'

const count = ref(0)
const internalMethod = () => {
  console.log('内部方法')
}

// 暴露公共API
defineExpose({
  increment() {
    count.value++
  },
  getCount: () => count.value,
  publicData: '可访问数据'
})

// 父组件通过 ref 访问:
// childRef.value.increment()

最佳实践总结

  1. 明确声明

    • 始终使用 defineProps 声明组件属性
    • 使用 defineEmits 明确组件事件
    • 使用 defineExpose 控制组件公共API
  2. 类型安全

    • 优先使用 TypeScript 类型声明
    • 为 props 添加验证逻辑
    • 为事件参数定义类型
  3. 封装原则

    • 只暴露必要的公共方法
    • 避免直接暴露内部状态
    • 使用前缀(如 _)表示私有属性
  4. 组合式API优势

    • 逻辑复用更简单
    • 类型推导更强大
    • 代码组织更灵活

这个示例展示了如何在实际组件中使用这三个核心宏,通过用户资料卡片的实现,您可以清楚地看到它们在实际开发中的应用场景和交互方式。

相关推荐
蓝胖子的多啦A梦12 分钟前
搭建前端项目 Vue+element UI引入 步骤 (超详细)
前端·vue.js·ui
骆驼Lara23 分钟前
前端跨域解决方案(1):什么是跨域?
前端·javascript
onebyte8bits28 分钟前
CSS Houdini 解锁前端动画的下一个时代!
前端·javascript·css·html·houdini
一点也不想取名1 小时前
解决 Java 与 JavaScript 之间特殊字符传递问题的终极方案
java·开发语言·javascript
涵信8 小时前
第一节 基础核心概念-TypeScript与JavaScript的核心区别
前端·javascript·typescript
小公主8 小时前
JavaScript 柯里化完全指南:闭包 + 手写 curry,一步步拆解原理
前端·javascript
TGB-Earnest10 小时前
【leetcode-合并两个有序链表】
javascript·leetcode·链表
GISer_Jing11 小时前
JWT授权token前端存储策略
前端·javascript·面试
拉不动的猪11 小时前
es6常见数组、对象中的整合与拆解
前端·javascript·面试
GISer_Jing11 小时前
Vue Router知识框架以及面试高频问题详解
前端·vue.js·面试