Vue 组件有哪些通讯方式?这里有10种方式及示例代码

你好同学,我是沐爸,欢迎点赞、收藏、评论和关注。

Vue 的组件通讯方式到底有几种?还真说不好。不过最常用、最常见的是以下十种:

props、 e m i t 、 emit、 emitparent/ c h i l d r e n 、 children、 childrenrefs、provide/inject、 a t t r s / attrs/ attrs/listeners、vuex/pinia、 eventBus/mitt、slot 作用域插槽、本地缓存

接下来一起看下各种通讯方式的特点、使用场景以及注意事项吧!Let’s go!

一、Props

  • 概述:通过 props 属性,父组件可以向子组件传递数据。
  • 特点:单向数据流,子组件不能直接修改 props 中的值(但可以通过触发事件请求父组件修改)。

父组件

<template>
  <child-component :count="count"></child-component>
</template>

<script>
  import { ref } from "vue";
  import ChildComponent from "./ChildComponent.vue";

  const count = ref(100);
</script>

子组件

<template>
  <div>{{ count }}</div>
</template>

<script setup>
  const props = defineProps(["count"]);

  console.log(props.count);
</script>

二、$emit

  • 概述:子组件可以通过 $emit 触发事件,并传递数据给父组件。
  • 特点:父组件需要监听这些事件来接收数据。

父组件

<template>
  <child-component
    :count="count"
    @increment="count += $event"
  ></child-component>
</template>

<script setup>
  import { ref } from "vue";
  import ChildComponent from "./ChildComponent.vue";

  const count = ref(100);
</script>

子组件

<template>
  <div>
    <div>{{ count }}</div>
    <button @click="$emit('increment', 2)">点一下</button>
  </div>
</template>

<script setup>
  const props = defineProps(["count"]);

  console.log(props.count);
</script>

注意,如果在子组件的 template 模板上使用 $emit,请确保子组件只有一个根节点,否则会报警告。如果不想增加额外根节点,解决警告的另一种方式是使用 defineEmits

<template>
  <div>{{ count }}</div>
  <button @click="handleClick">Increment</button>
</template>

<script setup>
  const props = defineProps(["count"]);

  console.log(props.count);

  const emit = defineEmits(["increment"]);

  function handleClick() {
    emit("increment", 2);
  }
</script>

三、$parent / $children

  • 概述:$parent 用于访问组件的父实例;$children 用于访问当前实例的直接子组件(仅 Vue2 支持)。
  • 特点:适用于简单的父子组件通信,但不建议在大型或复杂的项目中使用,因为它会增加组件间的耦合度。
  • 使用:在子组件中,使用 this.$parent 访问父组件;在父组件中,使用this.$children 访问子组件(但注意$children 是一个数组,需要指定索引)。

父组件

<template>
  <child-component :count="count" @increment="onIncrement"></child-component>
</template>

<script>
  import ChildComponent from "./ChildComponent.vue";

  export default {
    components: {
      ChildComponent,
    },
    data() {
      return {
        count: 100,
      };
    },
    methods: {
      onIncrement(e) {
        this.count += e;
        console.log(this.$children); // undefined
      },
    },
  };
</script>

子组件

<template>
  <div>
    <div>{{ count }}</div>
    <button @click="handleClick">点一下</button>
  </div>
</template>

<script>
  export default {
    props: ["count"],
    methods: {
      handleClick() {
        this.$parent.onIncrement(2);
      },
    },
  };
</script>

注意:

  • 在选项式组件中,无论是在子组件的 template 模板上使用 $emit,还是在<script>中使用 $emit,再或者使用this.$parent访问父组件的属性和方法,请确保子组件只有一个根节点,否则会报警告。
  • $parent 只能用在选项式组件中,子组件使用 this.$parent 访问父组件实例时,父组件也必须是选项式组件,请勿将选项式和组合式语法混用。

四、$refs

  • 概述:通过 $refs 可以访问组件实例或 DOM 元素。
  • 特点:主要用于父子组件之间的直接通信,但也可以用于访问任意组件实例。

选项式

父组件

<template>
  <button @click="giveMoney">给儿子零花钱</button>
  <child-component ref="child"></child-component>
</template>

<script>
  import ChildComponent from "./ChildComponent.vue";

  export default {
    components: {
      ChildComponent,
    },
    methods: {
      giveMoney() {
        this.$refs.child.changeMoney(1000);
      },
    },
  };
</script>

子组件

<template>
  <div>零花钱:{{ money }} 元</div>
</template>

<script>
  export default {
    data() {
      return {
        money: 1,
      };
    },
    methods: {
      changeMoney(n) {
        this.money += n;
      },
    },
  };
</script>

组合式

父组件

<template>
  <child-component
    ref="child"
    :count="count"
    @increment="onIncrement"
  ></child-component>
  <button @click="giveMoney">给儿子零花钱</button>
</template>

<script setup>
  import { ref } from "vue";
  import ChildComponent from "./ChildComponent.vue";

  const child = ref(null);

  function giveMoney() {
    child.value.changeMoney(1000);
  }
</script>

子组件

<template>
  <div>零花钱:{{ money }} 元</div>
</template>

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

  const money = ref(1);

  function changeMoney(n) {
    money.value += n;
  }

  // 子组件的数据和方法只有暴露出去,才能被外部访问到
  defineExpose({
    changeMoney,
  });
</script>

五、Provide / Inject

  • 概述:provideinject 允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深。
  • 特点:跨层级组件通信,但要注意不要滥用,以免造成组件间的耦合度过高。
  • 使用:在祖先组件中,使用 provide() 函数提供数据或方法;在子孙组件中,通过 inject 选项接收这些数据或方法。

祖先组件

<template>
  <div class="parent">
    <div>总资产:{{ money }} 元</div>
    <child-component></child-component>
  </div>
</template>

<script setup>
  import { provide, ref } from "vue";
  import ChildComponent from "./ChildComponent.vue";

  const money = ref(1000000000);

  function giveMoney(n) {
    money.value -= n;
  }

  provide("money", money);
  provide("giveMoney", giveMoney);
</script>

<style scoped>
  .parent {
    border: 1px solid red;
    padding: 10px;
  }
</style>

子组件

<template>
  <div class="child">
    <div>儿子的零花钱:{{ MyMoney }} 元</div>
    <button @click="handleClick">点一下</button>
    <grand-child></grand-child>
  </div>
</template>

<script setup>
  import GrandChild from "./GrandChild.vue";
  import { ref, inject } from "vue";

  const MyMoney = ref(1);
  const parentMoney = inject("money");
  const getMoney = inject("giveMoney");

  function handleClick() {
    if (parentMoney.value > 0) {
      MyMoney.value += 100;
      getMoney(100);
    }
  }
</script>

<style scoped>
  .child {
    border: 1px solid blue;
    padding: 10px;
    margin-top: 20px;
  }
</style>

孙组件

<template>
  <div class="grand-child">
    <div>孙子的零花钱:{{ MyMoney }} 元</div>
    <button @click="handleClick">点一下</button>
  </div>
</template>

<script setup>
  import { ref, inject } from "vue";

  const MyMoney = ref(0);
  const parentMoney = inject("money");
  const getMoney = inject("giveMoney");

  function handleClick() {
    if (parentMoney.value > 0) {
      MyMoney.value += 1000;
      getMoney(1000);
    }
  }
</script>

<style scoped>
  .grand-child {
    border: 1px solid green;
    padding: 10px;
    margin-top: 20px;
  }
</style>

六、$attrs / $listeners

  • $attrs 包含了组件所有透传 attributes。"透传 attribute"指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。
  • $listeners 包含了父作用域中的(不含.native 修饰符) v−on 事件监听器,可以通过 v−on="listeners" 传入内部组件。$listeners 只能在 Vue2 中使用,Vue3 中的透传的事件包含在 $attrs 中。

Vue3 语法

父组件

<template>
  <div class="parent">
    <div>总资产:{{ money }} 元</div>
    <child-component :money @click="giveMoney"></child-component>
  </div>
</template>

<script setup>
  import { ref } from "vue";
  import ChildComponent from "./ChildComponent.vue";

  const money = ref(1000000000);

  function giveMoney(e) {
    if (e.target) {
      return;
    }
    money.value -= e;
  }
</script>

<style scoped>
  .parent {
    border: 1px solid red;
    padding: 10px;
  }
</style>

子组件

<template>
  <div class="child">
    <div>子组件</div>
    <grand-child v-bind="$attrs"></grand-child>
  </div>
</template>

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

<style scoped>
  .child {
    border: 1px solid blue;
    padding: 10px;
    margin-top: 20px;
  }
</style>

孙组件

<template>
  <div class="grand-child">
    <h2>孙组件</h2>
    <div>祖先透传过来的数据: {{ $attrs }}</div>
    <button @click="getMoney">花钱</button>
  </div>
</template>

<script setup>
  import { useAttrs } from "vue";

  const attrs = useAttrs();
  console.log("孙组件", attrs);

  function getMoney() {
    attrs.onClick(1000);
  }
</script>

<style scoped>
  .grand-child {
    border: 1px solid green;
    padding: 10px;
    margin-top: 20px;
  }
</style>

giveMoney 可以作为一个方法而非事件进行传递:

<!-- 祖先组件 -->
<child-component :money :giveMoney></child-component>

<!-- 孙组件 -->
<button @click="getMoney">花钱</button>

function getMoney() { attrs.giveMoney(1000) }

在这里插入图片描述

Vue2 语法

祖先组件

<template>
  <div class="parent">
    <div>总资产:{{ money }} 元</div>
    <child-component :money="money" @click="giveMoney"></child-component>
  </div>
</template>

<script>
  import ChildComponent from "./ChildComponent.vue";

  export default {
    components: {
      ChildComponent,
    },
    data() {
      return {
        money: 1000000000,
      };
    },

    methods: {
      giveMoney(e) {
        if (e.target) {
          return;
        }
        this.money -= e;
      },
    },
  };
</script>

<style scoped>
  .parent {
    border: 1px solid red;
    padding: 10px;
  }
</style>

子组件

<template>
  <div class="child">
    <div>子组件</div>
    <grand-child v-bind="$attrs" v-on="$listeners"></grand-child>
  </div>
</template>

<script>
  import GrandChild from "./GrandChild.vue";
  export default {
    components: {
      GrandChild,
    },
    mounted() {
      console.log("子组件", this.$attrs, this.$listeners);
    },
  };
</script>

<style scoped>
  .child {
    border: 1px solid blue;
    padding: 10px;
    margin-top: 20px;
  }
</style>

孙组件

<template>
  <div class="grand-child">
    <h2>孙组件</h2>
    <div>祖先透传过来的数据: {{ $attrs }}</div>
    <button @click="getMoney">花钱</button>
  </div>
</template>

<script>
  export default {
    methods: {
      getMoney() {
        this.$listeners.click(1000);
      },
    },
    mounted() {
      console.log("孙组件", this.$attrs, this.$listeners);
    },
  };
</script>

<style scoped>
  .grand-child {
    border: 1px solid green;
    padding: 10px;
    margin-top: 20px;
  }
</style>

运行效果同上面的 Vue3 代码。

七、slot 作用域插槽

  • 概述:作用域插槽是插槽的一种特殊形式,它允许子组件将数据传递给插槽内容。这样,父组件就可以在插槽内容中访问子组件的数据,从而实现一种数据传递的效果。
  • 特点:作用域插槽使得组件间的数据传递更加灵活和强大,但也需要更加复杂的实现和理解。

父组件

<template>
  <div class="parent">
    <h3>父组件</h3>
    <child-component>
      <template #header="headerProps">
        <div>头部-来自作用域插槽的内容:{{ headerProps }}</div>
      </template>

      <template #default="defaultProps">
        <div>主体-来自作用域插槽的内容:{{ defaultProps }}</div>
      </template>

      <template #footer="footerProps">
        <div>底部-来自作用域插槽的内容:{{ footerProps }}</div>
      </template>
    </child-component>
  </div>
</template>

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

<style scoped>
  .parent {
    border: 1px solid red;
    padding: 10px;
  }
</style>

子组件

<template>
  <div class="child">
    <h3>子组件</h3>
    <slot name="header" message="hello header"></slot>
    <slot message="hello default"></slot>
    <slot name="footer" message="hello footer"></slot>
  </div>
</template>

<style scoped>
  .child {
    border: 1px solid blue;
    padding: 10px;
    margin-top: 20px;
  }
</style>

八、全局事件总线(EventBus / mitt)

  • EventBus 通过创建一个空的 Vue 实例作为全局事件总线,在组件间通过它来触发和监听事件。适用于任意组件间的通信,但需要注意事件名的冲突和事件清理。只有 Vue2 支持!
  • mitt 是一个在 Vue.js 应用程序及其他 JavaScript 环境中使用的小型事件总线库。适用于当传统的 props、emits、provide/inject 等方式不足以满足需求的场景。

EventBus

event-bus.js

import Vue from "vue";

export const EventBus = new Vue();

祖先组件

<template>
  <div class="parent">
    <div>总资产:{{ money }} 元</div>
    <button @click="handleClick">给孙子小明零花钱</button>
    <child-component></child-component>
  </div>
</template>

<script>
  import ChildComponent from "./ChildComponent.vue";
  import { EventBus } from "./event-bus";

  export default {
    components: {
      ChildComponent,
    },
    data() {
      return {
        money: 1000000000,
      };
    },
    methods: {
      handleClick() {
        this.money -= 1000;
        EventBus.$emit("give-money", 1000);
      },
    },
  };
</script>

<style scoped>
  .parent {
    border: 1px solid red;
    padding: 10px;
  }
</style>

子组件

<template>
  <div class="child">
    <div>子组件</div>
    <grand-child></grand-child>
    <grand-child2></grand-child2>
  </div>
</template>

<script>
  import GrandChild from "./GrandChild.vue";
  import GrandChild2 from "./GrandChild2.vue";
  export default {
    components: {
      GrandChild,
      GrandChild2,
    },
  };
</script>

<style scoped>
  .child {
    border: 1px solid blue;
    padding: 10px;
    margin-top: 20px;
  }
</style>

孙组件-小明

<template>
  <div class="grand-child">
    <h2>孙组件-小明</h2>
    <div>零花钱:{{ money }} 元</div>
    <button @click="handleClick">给妹妹小红零花钱</button>
  </div>
</template>

<script>
  import { EventBus } from "./event-bus";
  export default {
    data() {
      return {
        money: 0,
      };
    },
    methods: {
      handleClick() {
        if (this.money > 0) {
          this.money--;
          EventBus.$emit("give-sister", 1);
        }
      },
    },
    mounted() {
      EventBus.$on("give-money", (value) => {
        this.money += value;
      });
    },
    beforeDestroy() {
      EventBus.$off("give-money");
    },
  };
</script>

<style scoped>
  .grand-child {
    border: 1px solid green;
    padding: 10px;
    margin-top: 20px;
  }
</style>

孙组件-小红

<template>
  <div class="grand-child">
    <h2>孙组件-小红</h2>
    <div>零花钱:{{ money }} 元</div>
  </div>
</template>

<script>
  import { EventBus } from "./event-bus";
  export default {
    data() {
      return {
        money: 0,
      };
    },
    mounted() {
      EventBus.$on("give-sister", (value) => {
        this.money += value;
      });
    },
    beforeDestroy() {
      EventBus.$off("give-sister");
    },
  };
</script>

<style scoped>
  .grand-child {
    border: 1px solid green;
    padding: 10px;
    margin-top: 20px;
  }
</style>

在这里插入图片描述

mitt

安装

npm install --save mitt

Emitter.js

import mitt from "mitt";

export const Emitter = mitt();

祖先组件

<template>
  <div class="parent">
    <div>总资产:{{ money }} 元</div>
    <button @click="handleClick">给孙子小明零花钱</button>
    <child-component></child-component>
  </div>
</template>

<script setup>
  import ChildComponent from "./ChildComponent.vue";
  import { ref } from "vue";
  import { Emitter } from "./emitter";

  const money = ref(1000000000);

  function handleClick() {
    money.value -= 1000;
    Emitter.emit("give-money", 1000);
  }
</script>

<style scoped>
  .parent {
    border: 1px solid red;
    padding: 10px;
  }
</style>

子组件

<template>
  <div class="child">
    <h3>子组件</h3>
    <grand-child></grand-child>
    <grand-child2></grand-child2>
  </div>
</template>

<script setup>
  import GrandChild from "./GrandChild.vue";
  import GrandChild2 from "./GrandChild2.vue";
</script>

<style scoped>
  .child {
    border: 1px solid blue;
    padding: 10px;
    margin-top: 20px;
  }
</style>

孙组件-小明

<template>
  <div class="grand-child">
    <h2>孙组件-小明</h2>
    <div>零花钱:{{ money }} 元</div>
    <button @click="handleClick">给妹妹小红零花钱</button>
  </div>
</template>

<script setup>
  import { onBeforeUnmount, onMounted, ref } from "vue";
  import { Emitter } from "./emitter";

  const money = ref(0);

  function handleClick() {
    if (money.value > 0) {
      money.value--;
      Emitter.emit("give-sister", 1);
    }
  }

  onMounted(() => {
    Emitter.on("give-money", (value) => {
      money.value += value;
    });
  });

  onBeforeUnmount(() => {
    Emitter.off("give-money");
  });
</script>

<style scoped>
  .grand-child {
    border: 1px solid green;
    padding: 10px;
    margin-top: 20px;
  }
</style>

孙组件-小红

<template>
  <div class="grand-child">
    <h2>孙组件-小红</h2>
    <div>零花钱:{{ money }} 元</div>
  </div>
</template>

<script>
  import { EventBus } from "./event-bus";
  export default {
    data() {
      return {
        money: 0,
      };
    },
    mounted() {
      EventBus.$on("give-sister", (value) => {
        this.money += value;
      });
    },
    beforeDestroy() {
      EventBus.$off("give-sister");
    },
  };
</script>

<style scoped>
  .grand-child {
    border: 1px solid green;
    padding: 10px;
    margin-top: 20px;
  }
</style>

效果同 EventBus。

九、Vuex / Pinia

  • 概述:Vuex 和 Pinia 是 Vue.js 的状态管理模式和库,适用于大型应用,提供全局的状态存储。

Vuex

1.安装 Vuex

npm install vuex@next --save

2.配置 Vuex Store

import { createStore } from "vuex";

export default createStore({
  state() {
    return {
      count: 0,
    };
  },
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  mutations: {
    increment(state) {
      state.count++;
    },
    decrement(state) {
      state.count--;
    },
  },
  actions: {
    incrementIfOdd({ commit, state }) {
      if (state.count % 2 === 1) {
        commit("increment");
      }
    },
  },
});

3.在 Vue 应用中使用 Vuex Store

import { createApp } from "vue";
import App from "./App.vue";
import store from "./store";

const app = createApp(App);

app.use(store);
app.mount("#app");

4.在 Vue 组件中使用 Vuex Store

祖先组件

<template>
  <div class="parent">
    <h3>祖先组件</h3>
    <p>Count: {{ count }}</p>
    <p>Double Count: {{ doubleCount }}</p>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>

    <child-component></child-component>
  </div>
</template>

<script setup>
  import ChildComponent from "./ChildComponent.vue";
  import { computed } from "vue";
  import { useStore } from "vuex";

  const store = useStore();

  const count = computed(() => store.state.count);
  const doubleCount = computed(() => store.getters.doubleCount);

  function increment() {
    store.commit("increment");
  }

  function decrement() {
    store.commit("decrement");
  }
</script>

<style scoped>
  .parent {
    border: 1px solid red;
    padding: 10px;
  }
</style>

子组件

<template>
  <div class="child">
    <h3>子组件</h3>
    <grand-child></grand-child>
    <grand-child2></grand-child2>
  </div>
</template>

<script setup>
  import GrandChild from "./GrandChild.vue";
  import GrandChild2 from "./GrandChild2.vue";
</script>

<style scoped>
  .child {
    border: 1px solid blue;
    padding: 10px;
    margin-top: 20px;
  }
</style>

孙组件-1、孙组件-2

<template>
  <div class="grand-child">
    <h2>孙组件-1</h2>

    <p>Count: {{ count }}</p>
    <p>Double Count: {{ doubleCount }}</p>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
  </div>
</template>

<script setup>
  import { computed } from "vue";
  import { useStore } from "vuex";

  const store = useStore();

  const count = computed(() => store.state.count);
  const doubleCount = computed(() => store.getters.doubleCount);

  function increment() {
    store.commit("increment");
  }

  function decrement() {
    store.commit("decrement");
  }
</script>

<style scoped>
  .grand-child {
    border: 1px solid green;
    padding: 10px;
    margin-top: 20px;
  }
</style>

在这里插入图片描述

Pinia

1.安装 Pinia

npm install pinia --save

2.配置 Pinia store

// src/stores/counter.js

import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", {
  state: () => ({
    count: 0,
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    },
  },
});

3.在 Vue 应用中使用 Pinia Store

// src/main.js

import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";

const app = createApp(App);
const pinia = createPinia();

app.use(pinia);
app.mount("#app");

4.在 Vue 组件中使用 Pinia Store

祖先组件

<template>
  <div class="parent">
    <h3>祖先组件</h3>
    <p>Count: {{ counterStore.count }}</p>
    <p>Double Count: {{ counterStore.doubleCount }}</p>
    <button @click="counterStore.increment">Increment</button>
    <button @click="counterStore.decrement">Decrement</button>

    <child-component></child-component>
  </div>
</template>

<script setup>
  import ChildComponent from "./ChildComponent.vue";
  import { useCounterStore } from "@/stores/counter";

  const counterStore = useCounterStore();
</script>

<style scoped>
  .parent {
      border: 1px solid red
      padding: 10px
  }
</style>

子组件

<template>
  <div class="child">
    <h3>子组件</h3>
    <grand-child></grand-child>
    <grand-child2></grand-child2>
  </div>
</template>

<script setup>
  import GrandChild from "./GrandChild.vue";
  import GrandChild2 from "./GrandChild2.vue";
</script>

<style scoped>
  .child {
      border: 1px solid blue
      padding: 10px
      margin-top: 20px
  }
</style>

孙组件-1、孙组件-2

<template>
  <div class="grand-child">
    <h2>孙组件</h2>
    <p>Count: {{ counterStore.count }}</p>
    <p>Double Count: {{ counterStore.doubleCount }}</p>
    <button @click="counterStore.increment">Increment</button>
    <button @click="counterStore.decrement">Decrement</button>
  </div>
</template>

<script setup>
  import { useCounterStore } from "@/stores/counter";

  const counterStore = useCounterStore();
</script>

<style scoped>
  .grand-child {
      border: 1px solid green
      padding: 10px
      margin-top: 20px
  }
</style>

十、本地存储(LocalStorage/SessionStorage)

  • 概述:利用浏览器的本地存储功能,实现跨会话或跨标签页的组件通信。
  • 特点:适用于需要持久化数据的场景,但不适用于实时性要求较高的通信。同样也不适用于在同一页面多个组件共享数据的情形,因为某一组件更改了本地存储的数据,并不能响应式地更新相关组件的数据和视图。

好了,分享结束,谢谢点赞,下期再见。

  • 9
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沐爸muba

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值