vue3的组件通信

80 阅读7分钟

总结下vue3组件通信方法

  1. props父子传参
  2. emit自定义事件
  3. mitt事件总线
  4. v-model
  5. $attrs
  6. $refs、$parent
  7. provide、inject
  8. pinia
  9. slot

1. props父子传参

props变量可以实现父子组件传参
父组件通过props不仅可以传递变量,还可以传递函数给子组件

具体可查看vue官网props

1.父传子
父组件定义car变量,通过:car="car"传递给子组件
子组件通过defineProps接收car变量
2.子传父
父组件定义getToy方法,通过:getToy="getToy"传递给子组件,
子组件调用getToy方法,并将toy子组件变量作为getToy的参数传递给父组件

父组件

<template>
  <div class="father">
    <h3>这是父组件</h3>
    <h4>父组件的变量car:{{ car }}</h4>
    <h4>子组件通过getToy传递变量toy给父组件:{{ toy }}</h4>
    <!-- 父组件将car变量和getToy函数都传递给子组件 -->
    <Child :car="car" :getToy="getToy" />
  </div>
</template>
<script setup name="Father">
import { ref } from "vue";
import Child from "./Child.vue";

// 父组件定义car变量,通过props传递给子组件,即:car:"car"
const car = ref("小米");
const toy = ref("");

// 如何实现子组件传参
// 父组件定义getToy方法,通过props传递给子组件,即:getToy:"getToy"
function getToy(value) {
  // 定义的toy变量接收子组件传递过来的value(子组件变量toy),即实现子组件传参
  toy.value = value;
}
</script>

<style lang="scss" scoped>
.father {
  width: 500px;
  height: 400px;
  background-color: #f0ebeb;
  padding: 20px;
  text-align: left;
}
</style>

子组件

<template>
  <div class="child">
    <h3>这是子组件</h3>
    <h4>子组件的变量toy:{{ toy }}</h4>
    <h4>子组件接收父组件的变量car:{{ car }}</h4>
    <button @click="handleClick(toy)">点击将子组件变量toy传递给父组件</button>
  </div>
</template>

<script setup name="Child">
import { ref } from "vue";

const toy = ref("奥特曼");
// 子组件通过defineProps接收父组件传递的car变量和getToy方法
const props = defineProps(["car", "getToy"]);

// 点击事件
function handleClick(value) {
  // 子组件调用父组件传递过来的getToy方法,并把变量toy作为getToy的参数传入
  props.getToy(value);
}
</script>

<style lang="scss" scoped>
.child {
  width: 400px;
  height: 200px;
  background-color: #f1dbbb;
  padding: 20px;
}
</style>

2. 自定义事件

自定义事件用于实现子组件传参

  1. 子组件通过deineProps先声明自定义事件const emit = defineEmits(["sendToy"]);
    想要传参时触发emit("sendToy", "666")
  2. 父组件监听自定义事件,@send-toy="sendToy"

具体可查看vue官网组件事件

父组件

<template>
  <div class="father">
    <h3>这是父组件</h3>
    <h4>子组件通过自定义事件传递变量toy给父组件:{{ toy }}</h4>
    <!-- 给子组件child绑定自定义事件 -->
    <Child @sendToy="sendToy" />
  </div>
</template>
<script setup name="Father">
import { ref } from "vue";
import Child from "./Child.vue";

const toy = ref("");

// 如何实现子组件传参
function sendToy(value) {
  // 子组件传递过来的value(即变量toy),实现子组件传参
  toy.value = value;
}
</script>

<style lang="scss" scoped>
.father {
  width: 500px;
  height: 400px;
  background-color: #f0ebeb;
  padding: 20px;
  text-align: left;
}
</style>

子组件

<template>
  <div class="child">
    <h3>这是子组件</h3>
    <h4>子组件变量toy:{{ toy }}</h4>
    <!-- 方法一: 声明触发的事件-->
    <button @click="handleClick(toy)" class="btn">
      点击将子组件变量toy传递给父组件
    </button>
    <!-- 方法二:在 <template> 中直接使用$emit 方法 -->
    <button @click="$emit('sendToy', toy)">
      通过$emit将子组件变量toy传递给父组件
    </button>
  </div>
</template>

<script setup name="Child">
import { ref } from "vue";
// 声明触发的自定义事件,可通过 defineEmits() 宏来声明它要触发的事件
const emit = defineEmits(["sendToy"]);

const toy = ref("奥特曼");

// 点击事件
function handleClick(value) {
  // 触发sendToy事件
  emit("sendToy", value);
}
</script>

<style lang="scss" scoped>
.child {
  width: 400px;
  height: 200px;
  background-color: #f1dbbb;
  padding: 20px;
  .btn {
    margin-bottom: 20px;
  }
}
</style>

3. mitt事件总线

概述:与消息订阅与发布(pubsub)功能类似,可以实现任意组件间通信。

  1. 提供数据的组件,在合适的时候触发事件
  2. 接收数据的组件中:绑定事件、同时在销毁前解绑事件

Vue2.x 使用 EventBus 事件总线进行兄弟组件通信,而在Vue3中事件总线模式已经被移除,官方建议使用外部的、实现了事件触发器接口的库,例如 mitt

  1. 安装mitt
npm i mitt
  1. 新建文件:src\utils\emitter.ts
// 引入mitt 
import mitt from "mitt";

// 创建emitter
const emitter = mitt()

// 创建并暴露mitt
export default emitter
  1. 如何使用

父组件

<template>
  <div class="father">
    <h3>这是父组件</h3>
    <Child1 />
    <Child2 />
  </div>
</template>
<script setup name="Father">
import Child1 from "./Child1.vue";
import Child2 from "./Child2.vue";
</script>

<style lang="scss" scoped>
.father {
  width: 500px;
  height: 420px;
  background-color: #f0ebeb;
  padding: 20px;
  text-align: left;
}
</style>

子组件1

<template>
  <div class="child">
    <h3>这是子组件1</h3>
    <h4>子组件1的变量toy:{{ toy }}</h4>
    <button @click="handleClick(toy)">
      通过emitter将子组件1的变量toy传递给子组件2
    </button>
  </div>
</template>

<script setup name="Child">
import { ref } from "vue";
// 引入mitt
import emitter from "@/utils/emitter";

const toy = ref("奥特曼");
// 点击事件
function handleClick(value) {
  // 触发sendToy事件
  emitter.emit("sendToy", value);
}
</script>

<style lang="scss" scoped>
.child {
  width: 400px;
  height: 150px;
  background-color: #f1dbbb;
  padding: 20px;
  margin-bottom: 20px;
  .btn {
    margin-bottom: 20px;
  }
}
</style>

子组件2

<template>
  <div class="child">
    <h3>这是子组件2</h3>
    <h4>子组件2接收子组件1传递的变量toy:{{ toy }}</h4>
  </div>
</template>

<script setup name="Child">
import { ref } from "vue";
import { onMounted, onUnmounted } from "vue";
import emitter from "@/utils/emitter";

const toy = ref("");

onMounted(() => {
  // 监听sendToy事件
  emitter.on("sendToy", (value) => {
    toy.value = value;
  });
});

onUnmounted(() => {
  // 解绑事件
  emitter.off("sendToy");
  // 移除全部事件
  // emitter.all.clear()
});
</script>

<style lang="scss" scoped>
.child {
  width: 400px;
  height: 100px;
  background-color: #f1dbbb;
  padding: 20px;
  .btn {
    margin-bottom: 20px;
  }
}
</style>

4. v-model

概述:实现 父组件子组件 之间相互通信。

image.png

父组件

<template>
  <div class="father">
    <h3>这是父组件</h3>
    <h3>v-model绑定的变量userName: {{ userName }}</h3>
    <!-- 组件标签上使用v-model命令 -->
    <Child1 v-model="userName" />
    <!-- 本质为 -->
    <!-- <Child1 :modelValue="userName" @update:model-value="userName = $event" /> -->
  </div>
</template>
<script setup name="Father">
import { ref } from "vue";
import Child1 from "./Child1.vue";

let userName = ref();
</script>

<style lang="scss" scoped>
.father {
  width: 500px;
  height: 250px;
  background-color: #f0ebeb;
  padding: 20px;
  text-align: left;
}
</style>

子组件

<template>
  <div class="child">
    <h3>这是子组件</h3>
    <!--将接收的value值赋给input元素的value属性,目的是:为了呈现数据 -->
    <input type="text" :value="modelValue" @input="handleInput" />
  </div>
</template>

<script setup name="Child">
// 父组件通过v-model="xxx"的形式传递给子组件变量,子组件接收的变量默认为modelValue
const props = defineProps(["modelValue"]);
// 更新modelValue的方法默认为update:modelValue
const emit = defineEmits(["update:modelValue"]);

// 给input元素绑定原生input事件,调用handleInput方法时,进而触发update:model-value事件
const handleInput = ($event) => {
  emit("update:modelValue", $event.target.value);
};
</script>

<style lang="scss" scoped>
.child {
  width: 400px;
  height: 100px;
  background-color: #f1dbbb;
  padding: 20px;
  margin-bottom: 20px;
  .btn {
    margin-bottom: 20px;
  }
}
</style>

注意:如何更换modelValue

// 父组件
<!-- 通过v-model:xxx="yyy"的形式传递,即可将默认的modelValue修改为xxx -->
<Child v-model:mingzi="userName"/>
<!-- 上面代码的本质如下 -->
<Child :mingzi="userName" @update:mingzi="userName = $event"/>
   
// 子组件
<template>
  <div class="child">
    <input type="text" :value="mingzi" @input="handleInput" /> 
  </div>
</template>

<script setup name="Child">
// 父组件通过v-model:xxx="yyy"的形式传递给子组件变量,子组件接收的变量默认为xxx
const props = defineProps(["mingzi"]);
// 更新modelValue的方法默认为update:xxx
const emit = defineEmits(["update:mingzi"]);

// 给input元素绑定原生input事件,调用handleInput方法时,进而触发update:model-value事件
const handleInput = ($event) => {
  emit("update:mingzi", $event.target.value);
};
</script>

5.$attrs

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

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

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

父组件

<template>
  <div class="father">
    <h3>父组件</h3>
    <!-- v-bind="{ x: 400, y: 500 }" 相当于:x="x" :y="y",和v-bind="$attr"同理 -->
    <Child
      :a="a"
      :b="b"
      :c="c"
      v-bind="{ x: 400, y: 500 }"
      :updateA="updateA"
    />
  </div>
</template>

<script setup name="Father">
import Child from "./Child.vue";
import { ref } from "vue";
let a = ref(100);
let b = ref(200);
let c = ref(300);
// 方法也可传递
function updateA(value) {
  b.value = 0;
}
</script>

<style lang="scss" scoped>
.father {
  width: 500px;
  height: 500px;
  background-color: #f0ebeb;
  padding: 20px;
  text-align: left;
}
</style>

子组件

<template>
  <div class="child">
    <h3>子组件</h3>
    <h4>获取父组件传递的a: {{ a }}</h4>
    <!-- $attrs传递的变量包含父组件传递的所有属性,除去子组件中已经声明的属性a -->
    <GrandChild v-bind="$attrs" />
  </div>
</template>

<script setup name="Child">
import GrandChild from "./GrandChild.vue";
// 子组件中已声明属性a,其余父组件传递的属性都在$attr中
const props = defineProps(["a"]);
</script>

<style lang="scss" scoped>
.child {
  width: 400px;
  height: 450px;
  background-color: #f1dbbb;
  padding: 20px;
  margin-bottom: 20px;
  box-sizing: border-box;
  .btn {
    margin-bottom: 20px;
  }
}
</style>

孙组件

<template>
  <div class="grand-child">
    <h3>孙组件</h3>
    <h4>b:{{ b }}</h4>
    <h4>c:{{ c }}</h4>
    <h4>x:{{ x }}</h4>
    <h4>y:{{ y }}</h4>
    <button @click="updateA(666)">点击更新b变量为0</button>
  </div>
</template>

<script setup name="GrandChild">
defineProps(["b", "c", "x", "y", "updateA"]);
</script>

<style lang="scss" scoped>
.grand-child {
  width: 300px;
  height: 300px;
  background-color: #c3f4fa;
  padding: 20px;
  margin-bottom: 20px;
  box-sizing: border-box;
  .btn {
    margin-bottom: 20px;
  }
}
</style>

6.$parent$refs

属性说明
ref对dom元素或子组件添加ref属性,并声明一个与ref属性名称相同的变量,就能拿到该dom元素或者子组件实例
$refs值为对象,包含所有被ref属性标识的DOM元素或组件实例。用于父传子
$parent值为对象,当前组件的父组件实例对象。用于子传父

父组件

<template>
  <div class="father">
    <!-- 对dom元素h3添加ref属性c3 -->
    <h3 ref="c3">这是父组件</h3>
    <h4>父组件的变量toy: {{ toy }}</h4>
    <button @click="animalChange()" class="btn1">
      将子组件1的变量修改为"边牧安娜"
    </button>
    <!-- $refs为所有被`ref`属性标识的`DOM`元素或组件实例 -->
    <button @click="allChildGet($refs)">
      将两个子组件的变量都修改为“柯基”
    </button>
    <!-- 对子组件1添加ref属性c1 -->
    <Child1 ref="c1" />
    <!-- 对子组件1添加ref属性c2 -->
    <Child2 ref="c2" />
  </div>
</template>
<script setup name="Father">
import { ref } from "vue";
import Child1 from "./Child1.vue";
import Child2 from "./Child2.vue";

// 对子组件1添加ref属性,声明一个与ref属性名称相同的变量c1,c1即为实例子组件1
let c1 = ref();
// 对子组件2添加ref属性,声明一个与ref属性名称相同的变量c2,c2即为实例子组件2
let c2 = ref();
// 对dom元素h3添加ref属性,声明一个与ref属性名称相同的变量c3,c3即为DOM元素h3
let c3 = ref();

let toy = ref("迪迦");
// 将子组件1的变量dog修改为"边牧安娜"
const animalChange = () => {
  c1.value.dog = "边牧安娜";
};

const allChildGet = (refs) => {
  // refs为所有被`ref`属性标识的`DOM`元素或组件实例,包含c1,c2,c3
  console.log(/refs/, refs);

  refs["c1"].dog = "柯基";
  refs["c2"].dog = "柯基";
};

// 向外部提供数据
defineExpose({
  toy
});
</script>

<style lang="scss" scoped>
.father {
  width: 500px;
  height: 500px;
  background-color: #f0ebeb;
  padding: 20px;
  text-align: left;
  .btn1 {
    margin-right: 10px;
  }
}
</style>

子组件1

<template>
  <div class="child">
    <h3>这是子组件1</h3>
    <h4>子组件1的变量dog:{{ dog }}</h4>
    <button @click="changeBack($parent)">修改父组件的变量toy</button>
  </div>
</template>

<script setup name="Child">
import { ref } from "vue";

const dog = ref("边牧贝拉");

// 修改父组件变量toy
const changeBack = (parent) => {
  parent.toy = "加班";
};

// dog变量为子组件私有,需要通过defineExpose宏函数将变量交出去,外部才能读取该变量
defineExpose({
  dog
});
</script>

<style lang="scss" scoped>
.child {
  width: 400px;
  height: 150px;
  background-color: #f1dbbb;
  padding: 20px;
  margin-bottom: 20px;
  margin-top: 20px;
  .btn {
    margin-bottom: 20px;
  }
}
</style>

子组件2

<template>
  <div class="child">
    <h3>这是子组件2</h3>
    <h4>子组件2的变量dog:{{ dog }}</h4>
  </div>
</template>

<script setup name="Child">
import { ref } from "vue";

const dog = ref("萨摩耶大白");

// dog变量为子组件私有,需要通过defineExpose宏函数将变量交出去,外部才能读取该变量
defineExpose({
  dog
});
</script>

<style lang="scss" scoped>
.child {
  width: 400px;
  height: 100px;
  background-color: #f1dbbb;
  padding: 20px;
  .btn {
    margin-bottom: 20px;
  }
}
</style>

7. provideinject

  1. 概述:允许一个祖先组件向其所有子孙后代注入一个依赖,实现祖孙组件直接通信,查看官网provide、inject的使用
  2. 具体使用
属性说明
provide在祖先组件中通过provide配置向后代组件提供数据
inject在后代组件中通过inject配置来声明接收数据

父组件

<template>
  <div class="father">
    <h3>父组件</h3>
    <Child />
  </div>
</template>

<script setup name="Father">
import Child from "./Child.vue";
import { ref, reactive, provide } from "vue";
// 数据
let money = ref(100);
let dog = ref("边牧贝拉");
// 更新money
function updateMoney(value) {
  money.value += value;
}

// 要为组件后代提供数据,需要使用到 provide() 函数
// 第一个参数被称为注入名,可以是一个字符串或是一个 Symbol
// 第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref
provide("dog", dog);
// 和响应式数据配合使用,在供给方组件内声明并提供一个更改数据的方法函数
provide("moneyContext", { money, updateMoney });
</script>
<style lang="scss" scoped>
.father {
  width: 500px;
  height: 500px;
  background-color: #f0ebeb;
  padding: 20px;
  text-align: left;
}
</style>

子组件

<template>
  <div class="child">
    <h3>子组件</h3>
    <GrandChild />
  </div>
</template>

<script setup name="Child">
import GrandChild from "./GrandChild.vue";
</script>

<style lang="scss" scoped>
.child {
  width: 400px;
  height: 450px;
  background-color: #f1dbbb;
  padding: 20px;
  margin-bottom: 20px;
  box-sizing: border-box;
  .btn {
    margin-bottom: 20px;
  }
}
</style>

孙组件

<template>
  <div class="grand-child">
    <h3>孙组件</h3>
    <h4>零花钱:{{ money }}</h4>
    <h4>宠物:{{ dog }}</h4>
    <button @click="updateMoney(1)">点击零花钱+1</button>
  </div>
</template>

<script setup name="GrandChild">
// 要注入上层组件提供的数据,需使用 inject() 函数
import { inject } from "vue";
// 第一个参数为注入名,第二个参数为默认值,如果没有祖先组件提供 "dog",`value` 会是 "默认值"
let dog = inject("dog", "默认值");
// 注入数据
let { money, updateMoney } = inject("moneyContext", {
  money: 0,
  updateMoney: () => {}
});
</script>

<style lang="scss" scoped>
.grand-child {
  width: 300px;
  height: 300px;
  background-color: #c3f4fa;
  padding: 20px;
  margin-bottom: 20px;
  box-sizing: border-box;
  .btn {
    margin-bottom: 20px;
  }
}
</style>