Vue 3 组件通信的常用方式

464 阅读4分钟

Vue 3 组件通信的常用方式

Vue 3 是一个用于构建用户界面的渐进式 JavaScript 框架。组件是 Vue 的核心概念之一,它们可以封装可复用的代码片段,并且可以在整个应用中重复使用。为了实现组件之间的交互,Vue 提供了多种通信方式。

一、props/$emit

props是使用频率最高的一种通信方式,常用于 :父 ↔ 子

  • 父传子:属性值是非函数
  • 子传父:属性值是函数

特点:

  • 单向数据流:父组件的数据变化会流向子组件,但不会反过来。这有助于保持状态管理的清晰性和可预测性。
  • 静态props:组件实例化时就被确定,并且不会随时间变化的属性值,动态props:其值会根据父组件的数据状态而变化的属性
<my-component title="Static Title"></my-component>

<my-component :title="dynamicTitle"></my-component>
  • 推荐使用驼峰命名法

父组件:

<template>
  <div class="father" >
    <h3>父组件,</h3>
    <h4>我的车:{{ car }}</h4>
    <h4>儿子给的玩具:{{ toy }}</h4>
    <Child :car="car" handsome="帅气" :getToy="getToy" />
  </div>
</template>

<script setup>
  import Child from './Child.vue'
  import { ref } from "vue";
  // 数据
  const car = ref('奔驰')
  const toy = ref()
  // 方法
  function getToy(value){
    toy.value = value
  }
</script>


子组件:

<template>
    <div class="child">
      <h3>子组件</h3>
          <h4>我的玩具:{{ toy }}</h4>
          <h4>父给我的车:{{ car }}</h4>
          <h4>父给我的外貌:{{ handsome }}</h4>
          <button @click="invokeParent">玩具给父亲</button>
    </div>
  </template>
  
  <script setup>
      import { ref } from "vue";
      const toy = ref('奥特曼')
      
      let props = defineProps(['car','handsome','getToy'])
      function invokeParent() {
        props.getToy(toy.value)
      }
      
  </script>

screenshots.gif

$emit自定义事件常用于:子 => 父。

  • 子组件通过 $emit 触发自定义事件来通知父组件状态变化。

  • 原生事件:

    • 事件名是特定的(clickmosueenter等等)
    • 事件对象$event: 是包含事件相关信息的对象(pageXpageYtargetkeyCode
  • 自定义事件:

    • 事件名是任意名称
    • 事件对象$event: 是调用emit时所提供的数据,可以是任意类型!!!

父组件:

<template>
    <div>
        <h2>父组件</h2>
        <p>来自子组件的消息:{{ messageFromChild }}</p>
        <!-- 父子组件之间约定:子组件调用child-message事件传递消息给父组件,父组件监听child-message事件处理消息。 -->
        <ChildComponent @child-message="handleChildMessage" />
    </div>
</template>
<script setup>
// @ 事件  不是js的内置事件  是自定义事件
// 父子组件通信 
import { ref } from 'vue'
import ChildComponent from './components/ChildComponent.vue'
// 自定义事件 child-message  父子组件之间的约定 子组件调用child-message事件 传递消息给父组件
const messageFromChild = ref('')
// 处理子组件的消息  handleChildMessage 事件处理函数
const handleChildMessage = (message) => {
    console.log('父组件收到子组件的消息:', message)
    messageFromChild.value = message
}
</script>

子组件:

<template>
    <div>
        <h1>{{ title }}</h1>
        <button @click="sendMessageToParent">发送消息给父组件</button>
    </div>
</template>

<script setup>
import { 
    ref,
} from 'vue'
const childMessage = ref('Hello ,parent!')
// emits 触发 汇报
const emits = defineEmits(['child-message'])
const sendMessageToParent = () => {
    
    emits('child-message', childMessage.value)
}
</script>

image.png

直接传递函数和使用 $emit 触发事件在表面上看起来相似,因为两者都可以让子组件与父组件进行通信,然而,它们之间有几个关键的区别:

单向数据流

  • $emit: 遵循 Vue 的单向数据流原则。子组件通过触发事件通知父组件发生了什么,而不是直接调用父组件的方法。这使得数据流动清晰明了,便于理解和调试。
  • 直接传递函数: 子组件可以直接调用父组件提供的方法,这可能会模糊父子组件之间的界限,因为它打破了单向数据流的概念,使数据流变得不那么直观。

组件解耦

  • $emit: 使用 $emit 可以让子组件保持对父组件内部实现细节的无知。它只关心何时以及如何发出事件,而不关心父组件会怎样响应这些事件。这样可以提高组件的可复用性和独立性。
  • 直接传递函数: 直接传递函数的方式增加了父子组件间的耦合度。子组件需要知道父组件提供的具体函数,并且依赖于该函数的存在和签名。如果父组件中的函数逻辑发生变化,可能会影响到所有使用它的子组件。

代码清晰度

  • $emit: 有助于维持清晰的代码结构,特别是在大型项目中。它明确区分了“谁”(子组件)做了“什么”(触发了某个事件),以及“谁”(父组件)对此采取了行动。
  • 直接传递函数: 可能使代码逻辑变得不那么清晰,特别是当多个层级的组件相互调用时,可能会难以追踪数据流。

二、mitt

与消息订阅与发布(pubsub)功能类似,可以实现任意组件间通信。 mitt 是一个非常轻量级的事件发射器(event emitter)库,它没有依赖项,并且非常适合用于需要简单事件管理的场景。

  • 安装mitt
 npm i mitt
  • 新建文件:src\utils\emitter.js
import mitt from 'mitt';
const emitter = mitt();
export default emitter;
  • mitt 的 API

    • mitt() : 创建一个新的事件发射器实例。
    • on(type, handler) : 注册一个事件监听器,当指定类型的事件被触发时调用 handler 函数。
    • off(type, [handler]) : 移除指定类型的事件监听器。如果不提供 handler 参数,则移除所有该类型的监听器。
    • emit(type, [evt]) : 触发指定类型的事件,传递给所有已注册的监听器。evt 参数可以是任意类型的数据。

image.png

screenshots.gif

  • 生命周期管理:确保在组件卸载时清理不再需要的事件监听器,以防止内存泄漏。

三、v-model

v-model实现 父↔子 之间相互通信。

前序知识 —— v-model的本质

```
 <!-- 使用v-model指令 -->
 <input type="text" v-model="userName">
 ​
 <!-- v-model的本质是下面这行代码 -->
 <input 
   type="text" 
   :value="userName" 
   @input="userName =$event.target.value"
 >
```

如果在自定义组件上使用 v-model 而没有指定任何修饰符或额外配置,默认情况下它会:

  • 将父组件的数据通过一个名为 modelValue 的 prop 传递给子组件。
  • 监听子组件发出的一个名为 update:modelValue 的事件来更新父组件中的数据。
<!-- 组件标签上使用v-model指令 -->
<MyInput v-model="userName"/>

<!-- 组件标签上v-model的本质 -->
<MyInput :modelValue="userName" @update:model-value="userName = $event"/>

image.png

screenshots.gif

  • 多 v-model 支持:你可以通过修饰符的方式为同一个组件定义多个 v-model 绑定。
<MyInput v-model:abc="userName" v-model:xyz="password"/>

四、$attrs

$attrs用于实现当前组件的父组件,向当前组件的子组件通信(祖→孙)。

具体说明:$attrs是一个对象,包含所有父组件传入的标签属性。

注意:$attrs会自动排除props中声明的属性(可以认为声明过的 props 被子组件自己“消费”了)

父组件:

 <template>
   <div class="father">
     <h3>父组件</h3>
         <Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/>
   </div>
 </template>
 
 <script setup lang="ts" name="Father">
     import Child from './Child.vue'
     import { ref } from "vue";
     let a = ref(1)
     let b = ref(2)
     let c = ref(3)
     let d = ref(4)
 
     function updateA(value){
         a.value = value
     }
 </script>

子组件:

 <template>
     <div class="child">
         <h3>子组件</h3>
         <GrandChild v-bind="$attrs"/>
     </div>
 </template>
 
 <script setup lang="ts" name="Child">
     import GrandChild from './GrandChild.vue'
 </script>

孙组件:

 <template>
     <div class="grand-child">
         <h3>孙组件</h3>
         <h4>a:{{ a }}</h4>
         <h4>b:{{ b }}</h4>
         <h4>c:{{ c }}</h4>
         <h4>d:{{ d }}</h4>
         <h4>x:{{ x }}</h4>
         <h4>y:{{ y }}</h4>
         <button @click="updateA(666)">点我更新A</button>
     </div>
 </template>
 
 <script setup lang="ts" name="GrandChild">
     defineProps(['a','b','c','d','x','y','updateA'])
 </script>

五、refs/parent

概述:

-   `$refs`用于 :**父→子。**
-   `$parent`用于:**子→父。**

原理如下:

| 属性        | 说明                                |
| --------- | --------------------------------- |
| `$refs`   | 值为对象,包含所有被`ref`属性标识的`DOM`元素或组件实例。 |
| `$parent` | 值为对象,当前组件的父组件实例对象。

image.png

screenshots.gif

六、 provide/inject

概述:实现祖孙组件直接通信 具体使用:

-   在祖先组件中通过`provide`配置向后代组件提供数据
-   在后代组件中通过`inject`配置来声明接收数据

父组件

<script setup>
  import Child from './Child.vue'
  import { ref,reactive,provide } from "vue";
  // 数据
  let money = ref(100)
  let car = reactive({
    brand:'奔驰',
    price:100
  })
  // 用于更新money的方法
  function updateMoney(value:number){
    money.value += value
  }
  // 提供数据
  provide('moneyContext',{money,updateMoney})
  provide('car',car)
</script>

孙组件

<script setup>
  import { inject } from 'vue';
  // 注入数据
  //可以指定默认值
 let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(x:number)=>{}})
  let car = inject('car')

七、slot

通过使用 slot,你可以将内容从父组件传递到子组件中指定的位置,这使得子组件可以在不暴露其内部实现细节的情况下,灵活地嵌入外部提供的内容。

Slot 的基本类型

  • 默认插槽(Default Slot)

这是最简单的形式,当没有指定名称时,默认插槽会占据子组件中的 标签位置。

  • 具名插槽(Named Slot)

当你需要多个不同的插槽时,可以使用具名插槽。每个具名插槽都有一个唯一的名称,以便父组件能够明确地向哪个插槽插入内容。

  • 作用域插槽(Scoped Slot)

作用域插槽允许子组件向父组件传递数据。父组件可以通过模板语法访问这些数据,并据此渲染内容。实际上,这是父组件定义了如何展示子组件的数据。

父组件

<template>
  <div>
    <h2>我是父组件</h2>
    <!-- 使用默认插槽 -->
    <ChildComponent>
      <p>这是默认插槽的内容。</p>
    </ChildComponent>

    <!-- 使用具名插槽 -->
    <ChildComponent>
      <template #header>
        <p>这是头部内容。</p>
      </template>
      <template #default>
        <p>这是默认插槽的内容。</p>
      </template>
      <template #footer>
        <p>这是底部内容。</p>
      </template>
    </ChildComponent>

    <!-- 使用作用域插槽 -->
    <ChildComponent>
      <template #scoped="{ user }">
        <p>{{ user.name }} - {{ user.age }}</p>
      </template>
    </ChildComponent>
  </div>
</template>

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

子组件

<template>
  <div class="child-component">
    <!-- 默认插槽 -->
    <slot></slot>

    <hr />

    <!-- 具名插槽 -->
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot> <!-- 可以再次使用默认插槽 -->
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>

    <hr />

    <!-- 作用域插槽 -->
    <slot name="scoped" :user="user"></slot>
  </div>
</template>

<script setup>
// 定义要传递给作用域插槽的数据
const user = {
  name: '张三',
  age: 30,
};
</script>

八、pinia

之前写过pinia的基本使用,可以参考pinia入门,这里就不赘述了。