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

html 复制代码
<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

html 复制代码
<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 中,子组件向父组件传值可以通过触发自定义事件来实现。

  1. 在子组件中,可以使用defineEmits宏来定义可以触发的自定义事件。然后在需要的地方,使用emit函数触发事件并传递数据给父组件。
    在子组件的<script setup>部分,使用defineEmits定义可以触发的事件:
html 复制代码
<template>
  <button @click="sendDataToParent">Send Data</button>
</template>
<script setup lang="ts" name="ChildComponent">
import { defineEmits } from 'vue';
const emit = defineEmits(['childEvent']);
function sendDataToParent() {
  emit('childEvent', 'Data from child');
}
</script>
  1. 在父组件中,使用子组件标签时,可以通过v-on指令监听子组件触发的事件,并在事件处理函数中接收子组件传递过来的数据。
html 复制代码
<template>
  <div>
    <ChildComponent @childEvent="handleChildEvent" />
  </div>
</template>
<script setup>
import ChildComponent from './ChildComponent.vue';
function handleChildEvent(data) {
  console.log('打印子组件传递的数据:', data);
}
</script>

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

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

示例

在子组件中:

html 复制代码
<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 函数中触发这个事件并传递数据。

在父组件中:

html 复制代码
<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等事件。
html 复制代码
<!-- 使用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>
  1. 在自定义组件上:v-model的本质
    当在自定义组件上使用v-model时,需要在子组件中明确接收一个名为modelValueprop,并在数据变化时触发一个名为update:modelValue的自定义事件。
html 复制代码
<!-- 父组件 -->
<template>
  <div>
    <ChildComponent v-model="parentData" />
    <p>{{ parentData }}</p>
  </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事件,通知父组件更新数据。

多个v-model绑定

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

html 复制代码
<!-- 父组件 -->
<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修饰符:

    • 自动去除用户输入值两端的空格。
html 复制代码
<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可以用于实现当前组件的父组件 ,向当前组件的子组件 通信(祖→孙)。

示例

html 复制代码
<!-- Father.vue -->
<template>
  <div>
    <ChildComponent someAttribute="value" />
  </div>
</template>

<script setup name="Father">
import ChildComponent from './ChildComponent.vue';
</script>

<!-- ChildComponent.vue -->
<template>
  <div>
    <GrandchildComponent v-bind="$attrs" />
  </div>
</template>

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

<!-- GrandchildComponent.vue -->
<template>
  <!-- 可以直接通过$attrs访问属性 -->
  <div>{{ $attrs.someAttribute }}</div>
</template>

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

父组件向子组件传递了 someAttribute 属性,子组件没有通过 props 接收这个属性,而是将 $attrs 传递给了孙组件,孙组件可以通过 $attrs 访问到这个属性。

$refs$parent

  • $refs用于 :父→子。

  • $parent用于:子→父。

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

示例

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

<!-- ChildComponent.vue -->
<template>
  <div>子组件的值:{{ childValue }}</div>
  
  <button @click="changeParentData($parent)">修改父组件的值</button>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import GrandChild from './GrandChild.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>

provideinject

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

provideinject的作用范围是从提供数据或方法的组件开始,向下传递到所有后代组件。如果在中间的组件中再次提供相同键的数据或方法,会覆盖祖先组件提供的值。

注意,对注入的响应式数据的修改会影响所有注入了该数据的组件。

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

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

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

// 提供一个方法
function sharedMethod() {
  console.log('Shared method called.');
}
provide('methodKey', sharedMethod);

let injectedMoney = ref(100)
const injectedUpdateMoney = (value:number) => {
  money.value += value
}
// 提供一个名为 moneyContext 的依赖,其值是一个包含 money 和 updateMoney 的对象。
provide('moneyContext',{ injectedMoney, injectedUpdateMoney })
</script>
  • inject:在后代组件中,使用inject函数来注入祖先组件提供的数据或方法。
    • 第一个参数是与provide中相同的键,用于指定要注入的数据或方法。
    • 第二个参数是可选的默认值,如果祖先组件没有提供对应的值,则使用这个默认值。
html 复制代码
<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('methodKey');

function callInjectedMethod() {
  injectedMethod();
}

// 直接接收对象
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

shell 复制代码
npm install mitt
# 或者 yarn
yarn add mitt

订阅事件

  1. on(eventName, handler):使用on方法监听特定事件,并提供一个回调函数来处理事件。

    • eventName:要订阅的事件名称,类型为字符串或符号。
    • handler:事件触发时要执行的回调函数,该函数接收事件触发时传递的参数。
  2. once(eventName, handler)

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

示例:

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

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

触发事件

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

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

typescript 复制代码
emitter.emit('eventName', arg1, arg2,...);

取消订阅

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

示例:

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

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

清除所有订阅

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

示例:

typescript 复制代码
emitter.all.clear();

示例

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

typescript 复制代码
// @/utils/emitter.ts
import mitt from 'mitt';
const emitter = mitt();

// 创建并暴露mitt
export default emitter

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

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

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

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

typescript 复制代码
import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";

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

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

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

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

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

slot

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

默认插槽

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

html 复制代码
<template>
  <div>
    <h3>子组件</h3>
    <slot></slot>
  </div>
</template>

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

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

具名插槽

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

html 复制代码
<template>
  <div>
    <h3>子组件</h3>
    <slot name="content"></slot>
  </div>
</template>

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

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

作用域插槽

基本概念

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

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

html 复制代码
<template>
  <div>
    <h3>子组件</h3>
    <slot :data="slotData"></slot>
  </div>
</template>
<script setup>
import { ref } from 'vue';
const slotData = ref('这是子组件传递给插槽的数据');
</script>

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

html 复制代码
<template>
  <div>
    <ChildComponent>
      <!-- <template v-slot:default="{ data }"> 等同于 #default={data}-->
      <template #default="{ data }">
        <p>从子组件获取的数据:{{ data }}</p>
      </template>
    </ChildComponent>
  </div>
</template>

在这个例子中,父组件通过解构赋值获取了子组件传递的data,并在插槽内容中使用这个数据进行渲染。

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

动态插槽

通常情况下,插槽在组件中是固定的,一旦定义好了插槽的位置和名称,父组件就会按照这些固定的插槽来传递内容。但是,动态插槽允许在运行时根据不同的条件来切换使用不同的插槽,从而实现更加灵活的组件渲染。

在父组件中:

根据条件使用不同的插槽模板。可以使用v-ifv-else等指令来根据条件选择不同的插槽模板。

html 复制代码
<template>
  <div>
    <ChildComponent>
      <template v-if="showContentSlot">
        <template #content>
          <p>这是内容插槽的内容</p>
        </template>
      </template>
      <template v-else>
        <template #default>
          <p>这是默认插槽的内容</p>
        </template>
      </template>
    </ChildComponent>
  </div>
</template>
<script setup>
import { ref } from 'vue';
const showContentSlot = ref(true);
</script>

在子组件中:

定义相应的插槽,以便父组件可以根据条件传递不同的内容。

html 复制代码
<template>
  <div>
    <h3>子组件</h3>
    <slot name="content"></slot>
    <slot></slot>
  </div>
</template>

子组件中定义了一个命名插槽content和一个默认插槽,以接收父组件根据条件传递的内容。

动态插槽使得组件的渲染更加灵活,可以根据不同的条件动态地选择不同的插槽内容,适应不同的场景需求。

相关推荐
qq_5895681032 分钟前
Echarts+vue电商平台数据可视化——后台实现笔记
vue.js·信息可视化·echarts
2401_882727571 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
NoneCoder1 小时前
CSS系列(36)-- Containment详解
前端·css
anyup_前端梦工厂1 小时前
初始 ShellJS:一个 Node.js 命令行工具集合
前端·javascript·node.js
5hand2 小时前
Element-ui的使用教程 基于HBuilder X
前端·javascript·vue.js·elementui
GDAL2 小时前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js
六卿2 小时前
react防止页面崩溃
前端·react.js·前端框架
z千鑫2 小时前
【前端】详解前端三大主流框架:React、Vue与Angular的比较与选择
前端·vue.js·react.js
m0_748256143 小时前
前端 MYTED单篇TED词汇学习功能优化
前端·学习
小马哥编程4 小时前
Function.prototype和Object.prototype 的区别
javascript