Bootstrap

vue3 组件通信方式

在 Vue 3 中,组件通信是一个关键的概念,它允许我们在组件之间传递数据和事件。

vue3 组件通信方式

  • 父传子

    • props
    • v-model
    • $refs
    • 默认插槽、具名插槽、动态插槽
  • 子传父

    • props
    • v-model
    • $parent
    • 自定义事件
    • 作用域插槽
  • 祖传孙、孙传祖

    • $attrs
    • provideinject
  • 兄弟间、任意组件间

    • Pinia
    • mitt

Vue3组件通信和Vue2的区别

  • 移出事件总线,使用mitt代替。
  • vuex换成了pinia
  • .sync优化到了v-model里面了。
  • $listeners所有的东西,合并到$attrs中了。
  • $children被砍掉了。

props

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

  • 父传子:属性值是非函数
  • 子传父:属性值是函数
    • 使用 props 实现父传子,需要父组件先传递给子组件一个函数,子组件调用父组件给的函数实现子传父。

更多关于Props的知识点请查看vue3 Props的用法(父传子)

示例

父组件Father.vue

<template>
  <div>
    <h3>在父组件中</h3>
    <p>父组件的房子:{{ house }}</p>
    <p>父组件的车:{{ car}}</p>
    <p>子组件传给父组件的生日礼物:{{ birthdayGift }}</p>
    <ChildComponent :car="car" :giveBirthdayGift="getBirthdayGift" />
  </div>
</template>

<script setup lang="ts" name="Father">
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
// 数据
const house = ref('独栋小别墅')
const car = ref('奔驰');
const birthdayGift = ref();
// 方法
function getBirthdayGift(value: string) {
  birthdayGift.value = value;
}
</script>

子组件ChildComponent.vue

<template>
  <div>
    <h3>子组件</h3>
    <h4>子给父 买的生日礼物:{{ birthdayGift }}</h4>
    <h4>父给子的车:{{ car }}</h4>
    <button @click="giveBirthdayGift(birthdayGift )">把生日礼物送给父组件</button>
  </div>
</template>

<script setup lang="ts" name="ChildComponent">
import { ref } from 'vue';
const birthdayGift = ref('一份全A成绩单');

defineProps(['car', 'giveBirthdayGift']);
</script>

自定义事件

子组件向父组件传值可以通过触发自定义事件来实现。

在 Vue 3 中,defineEmits是一个用于定义组件发出的自定义事件的函数。

  • 可以以两种形式声明触发的事件:
    • 使用字符串数组的简易形式。数组中的元素是字符串,表示子组件可以触发的自定义事件名称。例如defineEmits(['eventName1', 'eventName2'])
    • 使用对象的完整形式。该对象的每个属性键是事件的名称,值是 null 或一个验证函数。
  • defineEmits函数的返回值是一个函数,通常命名为emitemit可以在子组件中触发自定义事件。当子组件需要向父组件传递数据或通知父组件发生了某些事情时,可以使用emit函数触发相应的自定义事件。
  1. 在子组件中,可以使用defineEmits宏来定义可以触发的自定义事件。然后在需要的地方,使用emit函数触发事件并传递数据给父组件。
    在子组件的<script setup>部分,使用defineEmits定义可以触发的事件:
<template>
  <button @click="sendDataToParent">Send Data</button>
  <!-- 也可以直接在模板中使用emit -->
  <button @click="emit('childEvent', 'Data from child')">Send Data</button>
</template>
<script setup lang="ts" name="ChildComponent">
import { defineEmits } from 'vue';
// 声明自定义事件并发挥emit函数
const emit = defineEmits(['childEvent']);
function sendDataToParent() {
  // 触发自定义事件
  emit('childEvent', 'Data from child');
}
</script>
  1. 在父组件中,使用子组件标签时,可以通过v-on指令监听子组件触发的事件,并在事件处理函数中接收子组件传递过来的数据。
<template>
  <div>
    <ChildComponent @childEvent="handleChildEvent" />
  </div>
</template>
<script setup>
import ChildComponent from './ChildComponent.vue';
function handleChildEvent(data) {
  console.log('打印子组件传递的数据:', data);
}
</script>

事件监听@childEvent="handleChildEvent" 表示父组件通过v-on指令(缩写为@)监听子组件触发的名为childEvent的自定义事件。当子组件触发这个事件时,父组件的handleChildEvent方法会被调用。

使用 defineEmits() 自定义事件时,注意区分原生事件与自定义事件:

  • 原生事件
    • 由浏览器或 Vue 内置的 DOM 元素触发的事件,事件名称是特定的。例如 clickmosueenterinput 等。
    • 事件对象$event: 是包含事件相关信息的对象,可以直接访问原生事件对象的各种属性和方法(pageXpageYtargetkeyCode
    • 可以直接在模板中使用 v-on 指令监听这些事件。
    • 原生事件可以通过事件冒泡和捕获机制在 DOM 树中传播。
  • 自定义事件
    • 由开发者在组件中定义并触发的事件,用于组件之间的通信。
    • 事件名是任意名称。要为自定义事件选择有意义且与原生事件不同的名称,避免命名冲突。
    • 事件对象$event: 是调用emit时所提供的数据,可以是任意类型
    • 自定义事件通常只在组件之间进行通信,不会自动冒泡或捕获。
      • 如果需要在组件层次结构中传播自定义事件,可以手动实现事件的传递和处理逻辑。

示例

在子组件中:

<script setup lang="ts" name="ChildComponent">
import { defineEmits } from 'vue';
const emit = defineEmits(['customEvent']);
function sendDataToParent() {
  const dataToSend = 'Some data from child';
  emit('customEvent', dataToSend);
}
</script>

使用 defineEmits() 定义了一个名为 customEvent 的自定义事件,然后在 sendDataToParent 函数中触发这个事件并传递数据。

在父组件中:

<template>
  <div>
    <ChildComponent @customEvent="handleChildEvent" />
  </div>
</template>
<script setup lang="ts" name="Father">
function handleChildEvent(dataFromChild) {
  console.log('Data from child:', dataFromChild);
}
</script>

父组件通过 v-on 指令监听 customEvent 自定义事件,并在 handleChildEvent 函数中接收子组件传递的数据。

v-model

v-model是一个用于在表单元素或组件上实现双向数据绑定的指令。它允许在父组件和子组件之间自动同步数据,使得数据的变化可以在两个方向上进行传递。

v-model的本质

v-model的本质是语法糖,它是一种方便的方式来实现父子组件之间的双向数据绑定。

  1. 对于原生表单元素(如<input><textarea><select>等),v-model实际上是结合了value属性(对于<input><textarea>)或selectedValue属性(对于<select>)以及相应的inputchange等事件。
<!-- 使用v-model指令 -->
<input type="text" v-model="message">

<!-- v-model的本质是下面这行代码 -->
<input 
  type="text" 
  :value="message" 
  @input="message=(<HTMLInputElement>$event.target).value"
/>

<!-- 对于一个<select>元素,v-model="selectedOption"相当于 -->
<select :value="selectedOption" @change="selectedOption = $event.target.value">
  <option value="option1">Option 1</option>
  <option value="option2">Option 2</option>
</select>

在Vue Devtools查看数据:
为了对比,我用了messageModelmessageValue分别代替上面例子中的2个inputmessage变量。

  1. 在自定义组件上:v-model的本质
    当在自定义组件上使用v-model时,需要在子组件中明确接收一个名为modelValueprop,并在数据变化时触发一个名为update:modelValue的自定义事件。
<!-- 父组件 -->
<template>
  <div>
    <p>父组件定义的数据:{{ parentData }}</p>
    
    <ChildComponent v-model="parentData" />
    <!-- 这句代码本质如下: -->
    <ChildComponent :modelValue="parentData" 
    	@update:modelValue="parentData = $event" />

  </div>
</template>
<script setup>
import ChildComponent from './ChildComponent.vue';
import { ref } from 'vue';
const parentData = ref('Initial value');
</script>

<!-- 子组件 -->
<template>
  <div>
    <!--将接收的value值赋给input元素的value属性,目的是:为了呈现数据 -->
   	<!--给input元素绑定原生input事件,触发input事件时,进而触发update:modelValue事件-->
    <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
  </div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
const props = defineProps({
  modelValue: String,
});
const emit = defineEmits(['update:modelValue']);
</script>

父组件通过v-modelparentData绑定到子组件,子组件接收modelValue prop 并在输入框的值变化时触发update:modelValue事件,通知父组件更新数据。

  • <ChildComponent v-model="parentData" />表示在父组件中使用名为ChildComponent的自定义组件,并通过v-model将父组件中的parentData与子组件进行双向绑定。
  • <ChildComponent :modelValue="parentData" @update:modelValue="parentData = $event" />明确地展示了v-model的本质实现:
    • :modelValue="parentData"将父组件的parentData值传递给子组件的modelValue属性,作为子组件的初始值。
    • @update:modelValue="parentData = $event"表示当子组件内部触发update:modelValue事件时,父组件会将新的值(即事件参数$event)赋给parentData,从而实现双向数据绑定。

多个v-model绑定

v-model默认是把数据绑定到属性value上。可以更换value,例如改成firstsecond等。

<!-- 父组件 -->
<template>
  <div>
    <ChildComponent v-model:first="firstData" v-model:second="secondData" />
  </div>
</template>
<script setup>
import ChildComponent from './ChildComponent.vue';
import { ref } from 'vue';
const firstData = ref('First initial value');
const secondData = ref(42);
</script>

修饰符

  1. .lazy修饰符:

    • 默认情况下,v-model在每次输入事件后都会同步更新数据。使用.lazy修饰符后,v-model只会在blur事件(表单元素失去焦点时)或按下回车键时更新数据。
  2. .number修饰符:

    • 如果输入的值是一个字符串,但希望将其转换为数字类型,可以使用.number修饰符。
  3. .trim修饰符:

    • 自动去除用户输入值两端的空格。
<template>
 <div>
   <input v-model.lazy="message">
   <input v-model.number="numberValue">
   <input v-model.trim="message">
 </div>
</template>

$attrs

$attrs是一个包含了父组件传递给当前组件的非 props 属性的对象。

作用和特点

  • 传递未被处理的属性:当父组件向子组件传递一些属性,但子组件没有通过 props 声明接收这些属性时,这些属性会被收集到 $attrs 对象中。这样可以方便地将这些属性继续传递给子组件内部的其他组件或元素。
  • 避免属性重复声明:如果子组件不需要对某些属性进行特殊处理,使用 $attrs 可以避免在子组件中重复声明这些属性,减少代码冗余。
  • 支持多级传递$attrs 可以在组件层级中逐级传递,使得属性可以方便地穿过多个中间组件,到达最终需要的组件。

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

示例

<!-- Father.vue -->
<template>
  <div>
    <ChildComponent :num="num" :updateValue="updateValue" />
  </div>
</template>

<script setup lang="ts" name="Father">
import ChildComponent from './ChildComponent.vue';
import { ref } from 'vue';
const num = ref(6)
const updateValue = (count:number) => {
  num.value = num.value + count
}
</script>

<!-- ChildComponent.vue -->
<template>
  <div>
  	<!-- 通过 v-bind 将 $attrs 传递给了孙组件 -->
    <GrandchildComponent v-bind="$attrs" />
  </div>
</template>

<script setup>
import GrandchildComponent from './GrandchildComponent.vue';
// 子组件没有通过 `props` 接收父组件传递的属性与方法
</script>

<!-- GrandchildComponent.vue -->
<template>
  <!-- 可以直接通过$attrs访问属性 -->
  <div>{{ $attrs.num }}</div>
  
  <!-- 调用updateValue方法,更新num属性 -->
  <button @click="updateValue(6)">点我更新Father.vue的sum</button>
</template>

<script setup>
// 也可以通过defineProps接收属性,接收后,不能再通过$attrs访问属性
defineProps(['num', updateValue])
</script>

父组件向子组件传递了 num 属性和updateValue方法,子组件没有通过 props 接收父组件传递的属性和方法,而是通过v-bind$attrs 传递给了孙组件,孙组件可以通过 $attrs 访问到属性和方法,并且可以操作方法更新sum属性。

$refs$parent

  • $refs,Vue内置属性,可以再模板中直接访问。用于 :父→子。

  • $parent,Vue内置属性,可在模板中直接访问。用于:子→父。

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

示例

<!-- Father.vue -->
<template>
  <div>父组件的值:{{ parentValue }}</div>
  <button @click="changeChildValue">修改子组件的值</button>
</template>
<script setup lang="ts" name="Father">
import { ref } from 'vue';
import ChildComponent from "./ChildComponent.vue";
const parentValue = ref(123)
const childRef = ref();
const changeChildValue = () => {
  childRef.value.childValue = '白白的云朵';
}
// 必须要把父组件的属性暴露出去,子组件才能访问到
defineExpose({parentValue})
</script>

<!-- ChildComponent.vue -->
<template>
  <div>子组件的值:{{ childValue }}</div>
  <!-- 通过内置属性$parent访问父组件暴露出来的属性、方法等 -->
  <button @click="changeParentData($parent)">修改父组件的值</button>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const childValue = ref('蓝蓝的天空')

const changeParentData = (parent: {[key: string]: any} | null) => {
  console.log(parent)
  if(parent != null) {
    console.log(parent.parentValue)
    parent.parentValue++
  }
}
// 必须要把子组件的属性暴露出去,父组件才能访问到
defineExpose({childValue})
</script>

通过$refs属性获取全部子组件的实例

假如有多个子组件,获取所有的子组件实例对象:

<template>
  <div>父组件的值:{{ parentValue }}</div>
  <ChildComponent1 ref="c1" />
  <ChildComponent2 ref="c2" />
  <!-- 内置属性$refs -->
  <button @click="getAllChild($refs)">获取所有的子组件实例对象</button>
</template>
<script setup lang="ts" name="Father">
import { ref } from 'vue';
import ChildComponent1 from "./ChildComponent.vue";
import ChildComponent2 from "./ChildComponent.vue";

// 通过c1.value和c2.value可以访问子组件的实例对象
const c1 = ref()
const c2 = ref()

// 通过$refs属性获取全部子组件的实例
const getAllChild = ($refs:{[key: string]: any}) => {
  console.log($refs)
}
</script>

查看console.log($refs)在控制台的打印结果:
在这里插入图片描述
通过$refs属性获取全部子组件的实例。

provideinject

provide和inject是一对用于实现依赖注入的组合式 API。它们允许祖先组件向其所有后代组件提供数据和方法,而无需通过层层传递props

provideinject的作用范围是从提供数据或方法的组件开始,向下传递到所有后代组件。如果在中间的组件中再次提供相同键的数据或方法,会覆盖祖先组件提供的值。
注意,对注入的响应式数据的修改会影响所有注入了该数据的组件。

  • provide:在祖先组件中,使用provide函数来提供数据或方法。
    • 第一个参数是一个键(通常是一个字符串),用于标识提供的数据或方法。
    • 第二个参数是要提供的值,可以是任何数据类型(如字符串、对象、函数等)或响应式对象
<script setup lang="ts">
import { provide, reactive, ref  } from 'vue';

// 提供一个字符串数据
provide('dataKey', 'Some data');

// 可以使用reactive或ref来创建响应式对象
const reactiveData = reactive({ message: 'Hello from ancestor' });
provide('reactiveDataKey', reactiveData);

// 提供一个方法,后代组件可以调用该方法来修改祖先的数据
function sharedMethod(value: string) {
  console.log('Shared method called.');
  reactiveData.message = value
}
provide('sharedMethod', sharedMethod);

let injectedMoney = ref(100)
const injectedUpdateMoney = (value:number) => {
  money.value += value
}
// 提供一个名为 moneyContext 的依赖,其值是一个包含 money 和 updateMoney 的对象。
provide('moneyContext',{ injectedMoney, injectedUpdateMoney })
</script>
  • inject:在后代组件中,使用inject函数来注入祖先组件提供的数据或方法。
    • 第一个参数是与provide中相同的键,用于指定要注入的数据或方法。
    • 第二个参数是可选的默认值,如果祖先组件没有提供对应的值,则使用这个默认值。
<template>
  <div>{{ injectedData }} - {{ injectedReactiveData.message }} - 
    <button @click="callInjectedMethod">Call Method</button>
  </div>
</template>
<script setup lang="ts">
import { inject } from 'vue';

// 注入字符串数据,提供默认值
const injectedData = inject('dataKey', 'Default data');
// 可以使用类型标记进行类型检查
const injectedData1 = inject<string>('dataKey', 'Default value');

// 注入响应式对象
const injectedReactiveData = inject('reactiveDataKey');

// 注入方法
const injectedMethod = inject('sharedMethod');
// 调用方法修改祖先组件的变量
function callInjectedMethod() {
  injectedMethod('hello,change message')
}

// 直接接收对象
const injectedMoneyContext = inject('moneyContext');
// 解构赋值
let { injectedMoney, injectedUpdateMoney } = inject('moneyContext',{ injectedMoney: 0, injectedUpdateMoney: (x:number)=>{} })

</script>

使用inject函数从祖先组件注入名为moneyContext的依赖。如果祖先组件没有提供这个依赖,将使用默认值{ injectedMoney: 0, injectedUpdateMoney: (x: number) => {} },即一个包含初始值为 0injectedMoney和一个空的更新函数updateMoney
使用解构赋值语法可以方便地从注入的对象中提取特定的属性。在这里,将注入的对象中的injectedMoneyinjectedUpdateMoney属性提取出来,分别赋值给当前组件中的同名变量,使得在当前组件的代码中可以直接使用这些变量来访问和操作注入的值和方法。

Pinia

查看 Vue3 官方推荐状态管理库Pinia

mitt

mitt是一个小型的事件发射器库,类似于EventEmitter
在 Vue 中可以使用mitt实现组件之间的全局事件通信,或者在一些特定场景下替代 Vue 的内置事件系统。

基本概念

接收数据的:提前绑定好事件(提前订阅消息)
提供数据的:在合适的时候触发事件(发布消息)

安装mitt

npm install mitt
# 或者 yarn
yarn add mitt

订阅事件

  1. on(eventName, handler):使用on方法订阅特定的事件,并提供相应的处理函数。

    • eventName:要订阅的事件名称,类型为字符串或符号。
    • handler:事件触发时要执行的回调函数,该函数接收事件触发时传递的参数。
    • 使用方式:emitter.on( 'eventName', () => {} )
  2. once(eventName, handler)

    • on 类似,但该回调函数只会在事件第一次触发时执行一次。

示例:

emitter.on('customClick', (data) => {
  console.log('customClicked! Data:', data);
});

emitter.once('customLoad', (data) => {
  console.log('customLoad once! Data:', data);
});

触发事件

  • emit(eventName,...args):使用emit方法触发特定的事件,并可以传递数据给订阅者。
    • eventName:要触发的事件名称,类型为字符串或符号。
    • ...args:可选的参数,将传递给订阅该事件的回调函数。

使用emit方法触发事件,并传递任意数量的参数:

emitter.emit('eventName', arg1, arg2,...);

取消订阅

  • off(eventName, handler)
    • eventName:要取消订阅的事件名称,类型为字符串或符号。如果不提供此参数,则取消所有事件的订阅。
    • handler:要取消的特定回调函数。如果不提供此参数,则取消指定事件名称的所有回调函数。

示例:

const clickHandler = (data) => {
  console.log('customClicked! Data:', data);
};
emitter.on('customClick', clickHandler);

// 稍后取消订阅
emitter.off('customClick', clickHandler);

清除所有订阅

  • all.clear():清除事件发射器实例上的所有订阅。

示例:

emitter.all.clear();

示例

在项目中导入mitt并创建一个事件发射器实例:

// @/utils/emitter.ts
import mitt from 'mitt';
// 调用mitt创建事件发射器实例emitter,emitter可以触发事件、订阅事件等
const emitter = mitt();

// 创建并暴露mitt
export default emitter

提供数据的组件,在合适的时候触发事件:

import emitter from "@/utils/emitter";
import { ref } from 'vue';
const birthdayGift = ref('一份全A成绩单');

function giveBirthdayGift(){
  // 触发事件
  emitter.emit('giveBirthdayGift', birthdayGift.value)
}

在接收数据的组件中,绑定事件、同时在组件销毁前解绑事件:

import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";

// 绑定事件
emitter.on('getBirthdayGift',(value:string)=>{
  console.log('getBirthdayGift事件被触发', value)
})

onUnmounted(()=>{
  // 解绑事件
  emitter.off('getBirthdayGift')
})

可以在 Vue 项目中将mitt的事件发射器实例作为全局变量,在不同的组件中进行事件的触发和监听,实现全局事件通信。

// 在 main.ts 或其他全局入口文件中
import mitt from 'mitt';
const emitter = mitt();
app.config.globalProperties.$emitter = emitter;

在使用mitt时,要考虑与 Vue 的生命周期的结合,确保事件的触发和监听在正确的时机进行。

Slot

slot(插槽)允许父组件向子组件传递内容,使得子组件具有更高的可扩展性和灵活性。

默认插槽

如果子组件中只有一个未命名的<slot>元素,它就是默认插槽:

<template>
  <div>
    <h3>子组件</h3>
    <slot></slot>
  </div>
</template>

父组件中可以直接在子组件标签内部传递内容,这些内容将被插入到子组件的默认插槽中:

<template>
  <div>
    <ChildComponent>
      <p>这是传递给子组件默认插槽的内容</p>
    </ChildComponent>
  </div>
</template>

具名插槽

子组件中可以使用<slot>元素来定义插槽,并可以为插槽指定一个名称:

<template>
  <div>
    <h3>子组件</h3>
    <slot name="content"></slot>
  </div>
</template>

父组件中使用带slot属性的元素来向子组件的特定插槽传递内容:

<template>
  <div>
    <ChildComponent>
      <template #content>
        <p>这是传递给子组件插槽的内容</p>
      </template>
    </ChildComponent>
  </div>
</template>

作用域插槽

基本概念

  • 传统插槽(非作用域插槽):父组件向子组件传递静态内容,子组件在特定位置渲染这些内容。父组件无法直接访问子组件内部的数据来动态决定插槽内容的渲染方式。
  • 作用域插槽:子组件可以将数据暴露给父组件,父组件在使用插槽时可以通过解构赋值等方式获取这些数据,并根据数据动态地渲染插槽内容。

子组件定义作用域插槽,在子组件的模板中,使用<slot>元素并通过v-bind绑定数据:

<template>
  <div>
    <h3>子组件</h3>
    <!-- 给内置的slot组件传递prop -->
    <slot :message="slotMsg" :title="slotTitle"></slot>
  </div>
</template>
<script setup>
import { ref } from 'vue';
const slotMsg = ref('这是子组件传递给插槽的数据');
const slotTitle = ref('这是子组件传递给插槽的数据');
</script>

<slot :message="slotMsg" :title="slotTitle"></slot>通过动态绑定属性的方式,将名为slotMsgslotTitle的数据传递给<slot>组件。<slot>组件拿到数据以后,会把数据继续传递给插槽的使用者。
这使得父组件在使用子组件时,可以通过这个数据来定制插槽的内容。

父组件使用作用域插槽,在父组件的模板中,使用带template的元素来包裹插槽内容,并通过解构赋值获取子组件传递的数据:

<template>
  <div>
    <ChildComponent>
      <!-- <template v-slot:default="{ message }"> 等同于 #default={message}-->
      <template #default="{ message }">
        <p>从子组件获取的信息:{{ message }}</p>
      </template>
    </ChildComponent>
    
    <ChildComponent>
      <!-- 也可以直接获取slot传递的全部数据 -->
      <!-- params包含title、message -->
      <template v-slot="params">
        <h3>从子组件获取的标题:{{ params.title}}</h3>
        <p>从子组件获取的信息:{{ params.message }}</p>
      </template>
    </ChildComponent>
  </div>
</template>

v-slot:default="{ message }"#default="{ message }"v-slot="params"都是获取默认插槽的数据。default是默认插槽的名字。

作用域插槽实现了从子组件向父组件的数据传递。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;