组件基础

组件的定义与注册

组件是 Vue.js 框架中实现 UI 复用与逻辑封装的核心机制,它扩展了基本 HTML 元素,本质上是具有预定义选项的 Vue 实例,能够将页面拆分为独立、可重用的功能单元,显著提升代码的可维护性与开发效率[1][17]。组件注册是使用组件的前提,Vue 3.5 支持全局注册与局部注册两种模式,适用于不同的应用场景。

全局注册通过 Vue.component() 方法实现,注册后的组件可在应用的任何模板中直接使用,无需额外导入。该方式适用于通用型基础组件,如按钮、输入框、图标等高频复用元素。其基本语法为:

javascript
复制代码
// 全局注册 Button 组件
Vue.component('BaseButton', {
  props: ['label'],
  template: '<button>{{ label }}</button>'
})

局部注册则通过组件选项中的 components 属性声明,仅在当前组件及其子组件范围内可用,避免全局命名冲突,适合页面级或业务逻辑紧密相关的组件。在单文件组件(SFC)中,局部注册通常与 ES 模块导入结合使用:

plaintext
复制代码
<!-- 局部注册 UserProfile 组件 -->
<script>
import UserProfile from './UserProfile.vue'

export default {
  components: {
    // 注册后可在模板中使用 <user-profile> 标签
    UserProfile
  }
}
</script>

注册方式对比

维度 全局注册 局部注册
作用域 整个应用 当前组件及子组件
命名冲突风险 高(需手动保证唯一性) 低(组件内作用域隔离)
构建体积 可能导致冗余(未使用也打包) 按需引入,优化体积
适用场景 通用基础组件(如按钮、表单) 页面级/业务组件

组件通信

组件通信是实现组件协作的核心能力,Vue 3.5 中最基础且常用的通信方式包括 props 数据传递emit 事件触发,二者共同构成了“父传子”与“子传父”的双向数据交互机制。

Props 数据传递用于父组件向子组件传递数据,子组件通过 props 选项声明可接收的参数,并可指定类型校验、默认值、必填性等约束。在 TypeScript 环境下,可通过 defineProps 宏结合泛型实现编译时类型检查,增强代码健壮性:

typescript
复制代码
<script setup lang="ts">
// 带类型校验的 props 声明
interface Props {
  id: number;                // 必选数字类型
  title: string;             // 必选字符串类型
  isPublished?: boolean;     // 可选布尔类型,默认 undefined
}
const props = defineProps<Props>();  // 编译时校验 props 类型
</script>

在 JavaScript 环境中,props 类型校验通过对象形式声明,支持多种内置类型与自定义验证函数:

javascript
复制代码
export default {
  props: {
    // 基础类型校验
    message: {
      type: String,
      required: true  // 标记为必填项
    },
    // 带默认值的数组类型
    items: {
      type: Array,
      default: () => ['默认项1', '默认项2']  // 数组/对象默认值需用函数返回
    },
    // 自定义验证函数
    score: {
      validator: (value) => value >= 0 && value <= 100
    }
  }
}

Emit 事件触发用于子组件向父组件传递数据或通知状态变化。子组件通过 this.$emit() 方法触发自定义事件,父组件通过 @事件名 语法监听并处理。在 Composition API 中,可通过 defineEmits 宏声明事件类型,实现类型约束:

typescript
复制代码
<script setup lang="ts">
// 声明可触发的事件类型
const emit = defineEmits<{
  (e: 'update:title', newTitle: string): void;  // 事件名+参数类型
  (e: 'delete', id: number): void;
}>();

// 触发事件示例
const handleTitleChange = (newVal: string) => {
  emit('update:title', newVal);  // 传递新标题给父组件
};
</script>

父-子组件通信完整案例

plaintext
复制代码
<!-- 父组件 Parent.vue -->
<template>
  <div class="parent">
    <!-- 父组件通过 props 传递 title 给子组件 -->
    <ChildComponent 
      :title="pageTitle" 
      @update-title="handleTitleUpdate"
    />
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: { ChildComponent },
  data() {
    return {
      pageTitle: '初始标题'
    };
  },
  methods: {
    handleTitleUpdate(newTitle) {
      // 接收子组件传递的新标题并更新
      this.pageTitle = newTitle;
    }
  }
};
</script>

<!-- 子组件 ChildComponent.vue -->
<template>
  <div class="child">
    <h2>{{ title }}</h2>
    <button @click="changeTitle">修改标题</button>
  </div>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      required: true
    }
  },
  methods: {
    changeTitle() {
      // 子组件通过 emit 触发事件,传递数据给父组件
      this.$emit('update-title', '更新后的标题');
    }
  }
};
</script>

数据流向原则
Vue 组件通信严格遵循 单向数据流 原则:父组件通过 props 传递的数据,子组件只能读取不能直接修改。若需变更,必须通过 emit 触发父组件方法,由父组件更新数据后再同步给子组件。这一机制确保了数据流向的可预测性,简化了复杂应用的状态管理[17]。

组件插槽

插槽(Slot)是实现组件内容分发的核心机制,允许父组件向子组件插入自定义内容,增强组件的灵活性与复用性。Vue 3.5 支持 默认插槽具名插槽作用域插槽 三种类型,覆盖不同的内容分发场景。

默认插槽用于子组件的单一内容分发区域,当子组件模板中存在未命名的 <slot> 标签时,父组件包裹在子组件标签内的内容会自动填充到该位置。典型应用如“卡片组件”的自定义内容区域:

plaintext
复制代码
<!-- 子组件 Card.vue -->
<template>
  <div class="card">
    <div class="card-content">
      <!-- 默认插槽:接收父组件传递的内容 -->
      <slot>默认内容(当父组件无内容时显示)</slot>
    </div>
  </div>
</template>

<!-- 父组件使用 -->
<template>
  <Card>
    <p>这是自定义的卡片内容</p>
    <button>卡片内按钮</button>
  </Card>
</template>

具名插槽通过 name 属性实现多区域内容分发,适用于子组件存在多个独立内容区域的场景(如卡片的头部、主体、底部)。父组件通过 v-slot:name 指令(简写为 #name)指定内容对应的插槽:

plaintext
复制代码
<!-- 子组件 AdvancedCard.vue -->
<template>
  <div class="advanced-card">
    <!-- 具名插槽:头部区域 -->
    <slot name="header"></slot>
    
    <!-- 具名插槽:主体区域 -->
    <slot name="body"></slot>
    
    <!-- 具名插槽:底部区域 -->
    <slot name="footer"></slot>
  </div>
</template>

<!-- 父组件使用 -->
<template>
  <AdvancedCard>
    <template #header>
      <h2>卡片标题</h2>
    </template>
    
    <template #body>
      <p>卡片主体内容...</p>
    </template>
    
    <template #footer>
      <button>操作按钮</button>
    </template>
  </AdvancedCard>
</template>

作用域插槽允许子组件向父组件传递数据,使父组件能根据子组件数据动态渲染内容。子组件通过 <slot> 标签的属性绑定数据,父组件通过 v-slot 接收并使用这些数据(Vue 2.6+ 统一使用 v-slot 语法替代旧版 slot-scope):

plaintext
复制代码
<!-- 子组件 List.vue -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <!-- 作用域插槽:传递 item 数据给父组件 -->
      <slot :item="item" :index="index">
        <!-- 默认渲染:当父组件未提供插槽内容时 -->
        {{ item.name }}
      </slot>
    </li>
  </ul>
</template>

<script>
export default {
  props: {
    items: {
      type: Array,
      required: true
    }
  }
};
</script>

<!-- 父组件使用 -->
<template>
  <List :items="products">
    <!-- 接收子组件传递的 item 和 index 数据 -->
    <template v-slot="{ item, index }">
      <div class="custom-item">
        {{ index + 1 }}. {{ item.name }} - ¥{{ item.price }}
      </div>
    </template>
  </List>
</template>

动态与异步组件

动态组件通过 <component :is="componentName"> 语法实现组件的动态切换,componentName 为当前要渲染的组件名称或组件选项对象。结合 <keep-alive> 标签可实现组件状态缓存,避免频繁创建/销毁组件导致的性能损耗:

plaintext
复制代码
<template>
  <div>
    <!-- 动态组件切换 -->
    <button @click="currentComponent = 'ComponentA'">组件 A</button>
    <button @click="currentComponent = 'ComponentB'">组件 B</button>
    
    <!-- keep-alive 缓存非活动组件 -->
    <keep-alive>
      <component :is="currentComponent"></component>
    </keep-alive>
  </div>
</template>

<script>
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';

export default {
  components: { ComponentA, ComponentB },
  data() {
    return {
      currentComponent: 'ComponentA'
    };
  }
};
</script>

<keep-alive> 提供了 includeexclude 属性控制缓存范围,以及 max 属性限制缓存组件数量,进一步优化内存占用:

plaintext
复制代码
<!-- 仅缓存 ComponentA 和 ComponentB -->
<keep-alive include="ComponentA,ComponentB" max="10">
  <component :is="currentComponent"></component>
</keep-alive>

异步组件通过 defineAsyncComponent 方法实现组件的按需加载,有效减小初始包体积,提升应用加载速度。其核心原理是将组件加载逻辑包装为 Promise,在组件需要渲染时才触发网络请求加载组件代码:

javascript
复制代码
import { defineAsyncComponent } from 'vue';

// 基础用法
const HeavyComponent = defineAsyncComponent(() => 
  import('./HeavyComponent.vue')
);

// 带加载/错误状态处理
const AsyncWithStatus = defineAsyncComponent({
  loader: () => import('./DataTable.vue'),
  // 加载状态组件
  loadingComponent: LoadingSpinner,
  // 错误状态组件
  errorComponent: ErrorState,
  // 加载超时时间(毫秒)
  timeout: 3000,
  // 延迟显示加载状态(避免闪烁)
  delay: 200
});

在路由场景中,异步组件常与 Vue Router 结合实现路由级别的按需加载,进一步优化首屏加载性能:

javascript
复制代码
// 路由配置中的异步组件
const routes = [
  {
    path: '/dashboard',
    component: defineAsyncComponent(() => import('./views/Dashboard.vue'))
  }
];

异步组件状态处理最佳实践

  1. 加载状态:提供清晰的加载指示器(如骨架屏),设置合理的 delay 避免短时间加载的闪烁
  2. 错误恢复:实现错误状态组件的重试机制,允许用户手动重新加载失败的组件
  3. 性能监控:通过 onError 回调记录组件加载失败信息,用于性能分析与问题排查

组件系统作为 Vue.js 的核心特性,通过组件注册、通信、插槽及动态异步加载等机制,构建了灵活高效的 UI 复用体系。开发者需根据应用规模与性能需求,合理选择组件注册方式,设计清晰的数据流向,并善用插槽与异步加载优化用户体验与应用性能。