看一看,创建、注册与父子组件通信的 3 种常用方式

89 阅读9分钟

Vue 里面有有好多组件,组件是 Vue 应用的基本构建块。通过拆分为独立、可复用的模块,从而提升代码的可维护性和复用性。

接下来,将从组件的创建与注册入手,讲解一下父子组件通信的 3 种核心方式(Props 传值、自定义事件、 refs 调用),从而增长下知识,也来消磨下时间。

1. 什么是组件?

组件(Component)是 Vue 中可复用的 Vue 实例,它包含模板(template)、逻辑(script)和样式(style),可以理解为 “自定义的 HTML 元素”。

例如,一个页面可以拆分为 HeaderContentFooter 三个组件,每个组件负责自己的功能,通过组合形成完整页面。

2. 组件的创建方式(.vue 单文件组件)

在 Vue 中,最推荐的组件创建方式是 单文件组件 ,即一个 .vue 文件对应一个组件,包含三个核心部分:

<!-- 组件文件名:MyComponent.vue -->
<template>
  <!-- 模板:组件的 HTML 结构 -->
  <div class="my-component">
    <h3>{{ title }}</h3>
    <p>{{ content }}</p>
  </div>
</template>

<script setup>
// 逻辑:组件的数据、方法等
import { ref } from 'vue';

// 组件内部状态
const title = ref('我是一个组件');
const content = ref('这是组件的内容');
</script>

<style scoped>
/* 样式:组件的样式(scoped 表示样式仅作用于当前组件) */
.my-component {
  padding: 16px;
  border: 1px solid #eee;
  border-radius: 4px;
}
</style>
  • <template>:必须有且只有一个根元素(Vue 3 支持多根元素),定义组件的结构;
  • <script setup>:Vue 3 推荐的组合式 API 语法,定义组件的响应式数据、方法等;
  • <style scoped>scoped 属性确保样式仅作用于当前组件,避免样式冲突(可选)。

二、组件的注册方式

创建组件后,需要 “注册” 才能在其他组件中使用。Vue 提供两种注册方式:局部注册全局注册

1. 局部注册(推荐)

局部注册的组件仅在当前组件内可用,避免全局污染,适合大多数场景。

步骤

  1. 导入组件;
  2. 在 components 选项中注册(Vue 3 <script setup> 中导入即注册,无需额外配置)。
<!-- 父组件:Parent.vue -->
<template>
  <div>
    <h2>父组件</h2>
    <!-- 使用局部注册的组件 -->
    <my-component></my-component>
  </div>
</template>

<script setup>
// Vue 3 <script setup> 中,导入组件后可直接使用(自动局部注册)
import MyComponent from './MyComponent.vue';
</script>

注意:组件名推荐使用 PascalCase(帕斯卡命名法,如 MyComponent)  或 kebab-case(短横线命名法,如 my-component) ,在模板中使用时两种格式均可(推荐 kebab-case 与 HTML 标签风格保持一致)。

2. 全局注册

全局注册的组件在整个应用中任何组件内都可直接使用,适合通用组件(如按钮、输入框)。

步骤:在应用初始化时,通过 app.component() 注册。

// main.js(Vue 3)
import { createApp } from 'vue';
import App from './App.vue';
// 导入全局组件
import MyButton from './components/MyButton.vue';

const app = createApp(App);

// 全局注册组件(参数:组件名,组件对象)
app.component('my-button', MyButton);

app.mount('#app');

注册后,在任何组件中无需导入即可直接使用:

<template>
  <!-- 直接使用全局注册的组件 -->
  <my-button></my-button>
</template>

全局注册的缺点

  • 即使组件未被使用,也会被打包到最终代码中,增加包体积;
  • 可能导致命名冲突(多个组件重名时覆盖)。

建议:优先使用局部注册,仅对高频复用的基础组件(如 ButtonInput)使用全局注册。

三、父子组件通信的 3 种常用方式

组件间通信是组件化开发的核心问题,其中父子组件通信是最基础、最常见的场景。以下是 3 种核心通信方式:

1. 父传子:Props 传递数据

场景:父组件向子组件传递数据(如配置项、初始值)。
原理:父组件通过 “自定义属性” 传递数据,子组件通过 props 选项接收数据。

步骤 1:子组件声明 Props

子组件通过 defineProps 宏(Vue 3 <script setup>)声明接收的属性,并指定类型、默认值等。

<!-- 子组件:Child.vue -->
<template>
  <div class="child">
    <h3>子组件</h3>
    <p>父组件传递的标题:{{ title }}</p>
    <p>父组件传递的数量:{{ count }}</p>
    <p>父组件传递的用户信息:{{ user.name }},{{ user.age }}岁</p>
  </div>
</template>

<script setup>
// 声明接收的 props(指定类型、默认值、是否必传)
const props = defineProps({
  // 字符串类型
  title: {
    type: String,
    required: true // 必传属性
  },
  // 数字类型,有默认值
  count: {
    type: Number,
    default: 0 // 默认值
  },
  // 对象类型(默认值需用函数返回,避免引用类型共享)
  user: {
    type: Object,
    default: () => ({ name: '未知', age: 0 })
  }
});

// 访问 props 数据(无需 .value,因为 props 是响应式的)
console.log(props.title); // 父组件传递的 title 值
</script>

步骤 2:父组件传递 Props

父组件在使用子组件时,通过 “自定义属性” 传递数据,属性名与子组件 props 声明的名称一致。

<!-- 父组件:Parent.vue -->
<template>
  <div class="parent">
    <h2>父组件</h2>
    <!-- 传递 Props 给子组件 -->
    <child 
      title="这是父组件传递的标题" 
      :count="num"  <!-- 传递响应式数据需用 v-bind 简写 : -->
      :user="userInfo"
    ></child>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue';
import Child from './Child.vue'; // 局部注册子组件

// 父组件的数据
const num = ref(100); // 响应式数字
const userInfo = reactive({ name: '张三', age: 20 }); // 响应式对象
</script>

注意事项

  • 单向数据流:Props 是 “只读” 的,子组件不能直接修改 props 的值(会报错)。若需修改,应通过 “子传父” 通知父组件修改源数据(见方式 2)。
  • 类型校验:通过 type 指定 props 类型(如 StringNumberObject),Vue 会在开发环境校验传递的数据类型,不匹配时报警告。
  • 响应式传递:父组件传递响应式数据(ref/reactive)时,子组件会自动感知数据变化(无需额外处理)。

2. 子传父:自定义事件($emit)

场景:子组件向父组件传递数据或通知父组件执行操作(如子组件按钮点击后通知父组件更新数据)。
原理:子组件通过 $emit 触发 “自定义事件” 并传递数据,父组件通过 v-on 监听事件并接收数据。

步骤 1:子组件触发自定义事件

子组件通过 defineEmits 宏(Vue 3 <script setup>)声明可触发的事件,然后通过 emit 方法触发事件并传递数据。

<!-- 子组件:Child.vue -->
<template>
  <div class="child">
    <h3>子组件</h3>
    <button @click="handleClick">点击向父组件传值</button>
    <input 
      type="text" 
      v-model="inputValue" 
      @input="handleInput"  <!-- 输入时触发事件 -->
    >
  </div>
</template>

<script setup>
import { ref } from 'vue';

// 声明可触发的自定义事件(参数为事件名数组)
const emit = defineEmits(['send-message', 'input-change']);

// 子组件内部数据
const inputValue = ref('');

// 按钮点击事件:触发 send-message 事件,传递数据
const handleClick = () => {
  // 触发事件并传递参数(可传多个)
  emit('send-message', 'Hello 父组件', 123);
};

// 输入事件:触发 input-change 事件,传递输入值
const handleInput = () => {
  emit('input-change', inputValue.value);
};
</script>

步骤 2:父组件监听自定义事件

父组件在使用子组件时,通过 v-on(简写 @)监听子组件触发的事件,并在事件处理函数中接收数据。

<!-- 父组件:Parent.vue -->
<template>
  <div class="parent">
    <h2>父组件</h2>
    <!-- 监听子组件的自定义事件 -->
    <child 
      @send-message="handleMessage" 
      @input-change="handleInputChange"
    ></child>
    <p>子组件传递的消息:{{ childMessage }}</p>
    <p>子组件输入的值:{{ childInputValue }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import Child from './Child.vue';

// 父组件存储子组件传递的数据
const childMessage = ref('');
const childInputValue = ref('');

// 处理 send-message 事件(接收子组件传递的参数)
const handleMessage = (msg, num) => {
  console.log('子组件传递的参数:', msg, num); // 输出 "Hello 父组件" 123
  childMessage.value = msg;
};

// 处理 input-change 事件
const handleInputChange = (value) => {
  childInputValue.value = value;
};
</script>

进阶:事件参数校验(Vue 3.3+)

Vue 3.3+ 支持对自定义事件的参数进行类型校验,增强代码健壮性:

<script setup>
// 用对象形式声明事件,指定参数类型
const emit = defineEmits({
  // 校验 send-message 事件的参数(msg 为 string,num 为 number)
  'send-message': (msg, num) => {
    if (typeof msg !== 'string' || typeof num !== 'number') {
      console.warn('send-message 事件参数类型错误');
      return false; // 校验失败
    }
    return true; // 校验成功
  }
});
</script>

3. 父调用子:refs 获取子组件实例 / 元素

场景:父组件需要直接访问子组件的属性或方法(如调用子组件的初始化方法、获取子组件的 DOM 元素)。
原理:父组件通过 ref 属性给子组件指定标识,然后通过 $refs 访问子组件实例或 DOM 元素。

步骤 1:子组件暴露属性和方法

子组件通过 defineExpose 宏(Vue 3 <script setup>)主动暴露需要被父组件访问的属性和方法(<script setup> 中声明的内容默认是私有的)。

<!-- 子组件:Child.vue -->
<template>
  <div class="child" ref="childDom">
    <h3>子组件</h3>
    <p>子组件计数器:{{ count }}</p>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';

// 子组件内部状态
const count = ref(0);
// 子组件DOM元素(通过 ref 获取)
const childDom = ref(null);

// 子组件方法:增加计数
const increment = () => {
  count.value++;
};

// 子组件方法:重置计数
const reset = () => {
  count.value = 0;
};

// 组件挂载后打印DOM元素
onMounted(() => {
  console.log('子组件DOM:', childDom.value);
});

// 暴露属性和方法给父组件(父组件通过 refs 只能访问这里暴露的内容)
defineExpose({
  count, // 暴露响应式数据
  increment, // 暴露方法
  reset,
  childDom // 暴露DOM元素引用
});
</script>

步骤 2:父组件通过 refs 访问子组件

父组件在子组件上添加 ref 属性指定名称,然后通过 ref 变量访问子组件暴露的内容(需在组件挂载后访问,否则为 null)。

<!-- 父组件:Parent.vue -->
<template>
  <div class="parent">
    <h2>父组件</h2>
    <!-- 给子组件添加 ref 属性 -->
    <child ref="childRef"></child>
    <button @click="callChildMethod">调用子组件方法</button>
    <button @click="getChildData">获取子组件数据</button>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import Child from './Child.vue';

// 创建 ref 变量关联子组件(名称与模板中 ref 属性一致)
const childRef = ref(null);

// 组件挂载后才能访问到子组件实例
onMounted(() => {
  console.log('子组件实例:', childRef.value); // 输出子组件暴露的内容
});

// 调用子组件的方法
const callChildMethod = () => {
  if (childRef.value) {
    childRef.value.increment(); // 调用子组件的 increment 方法
  }
};

// 获取子组件的数据
const getChildData = () => {
  if (childRef.value) {
    console.log('子组件当前计数:', childRef.value.count.value); // 注意 ref 数据需 .value
    console.log('子组件DOM:', childRef.value.childDom.value); // 访问子组件的DOM
  }
};
</script>

注意事项

  • 访问时机:子组件的 ref 变量在组件挂载前为 null,需在 onMounted 钩子或点击事件等挂载后触发的逻辑中访问。
  • 避免过度使用refs 打破了组件的封装性,父组件直接依赖子组件的内部实现,不利于维护。优先使用 Props 和自定义事件,仅在必要时(如调用子组件方法)使用 refs
  • 响应式访问:若访问子组件暴露的 ref 数据(如 childRef.value.count),需通过 .value 访问其值(因为 count 是 ref 类型)。

四、总结

组件是 Vue 开发的核心,掌握以下要点能大幅提升组件化开发能力:

  1. 组件创建:使用 .vue 单文件组件,分离模板、逻辑和样式;

  2. 组件注册:优先局部注册,通用组件全局注册;

  3. 父子通信

    • 父传子:通过 Props 传递数据,子组件声明接收并使用;
    • 子传父:子组件 emit 自定义事件传递数据,父组件监听事件;
    • 父调子:通过 refs 访问子组件暴露的属性和方法(谨慎使用)。

总之,合理进行拆分组件,处理好界面之间的交互,能让项目结构清晰,易于开发。就像搞好男女朋友那样,后续还不是水到渠成吗?