Vue快速上手:六、多组件及组件间的通信

102 阅读10分钟

使用组件

使用其它组件有三个步骤:

  • 1、导入组件:使用import.vue文件导入本组件。
  • 2、注册组件:使用components属性注册组件。
  • 3、在模板板中使用:在template部分直接使用组件标签即可,标签名即组件名(组件名首字母请大写)
<script>
    import HelloWord from "./components/HelloWord.vue"; // 导入组件文件

    export default {
        components: { // 在此注册HelloWord组件
            HelloWord
        },
        setup() {
            // 执行代码
        }
    }
</script>

<template>
    <HelloWord /> <!-- 根据组件名使用组件标签 -->
</template>

上面这是组合式完整的写法,下面是语法糖写法(推荐使用)

<script setup>
    import HelloWord from "@/components/HelloWord.vue";
</script>

<template>
    <HelloWord />
</template>

前面就说过script setup会自动帮组件导出和处理setup方法,这里同样也自动帮我们完成了组件的注册。

局部注册

像以上这种在当前组件注册的组件只能在当前组件使用,如果要在其它组件使用的话,还得重新完成以上三步,这种方式就叫局部注册组件。

全局注册

如果想在整个vue项目中随意使用的话就得在全局注册这个组件,在项目工程的main.js中的Vue应用实例中使用component方法去全局注册。

如:main.js

import App from './App.vue' // 导入根组件
import HelloWord from "@/components/HelloWord.vue"; // 导入要注册的全局组件

const app = createApp(App); // 创建vue应用
app.component('HelloWord', HelloWord); // 将HelloWord组件注册为全局组件
app.mount("#app"); // 将app应用挂载到页面元素中

其它vue文件

<template>
    <HelloWord /> <!-- 全局组件,直接使用即可,不用在当前组件内再去注册了 -->
</template>

尽管可以全局注册组件,但是除非必要不太推荐使用全局注册组件,全局组件虽然使用方便但是不推荐,例如:

  • 不同的开发者或者不同的组件库可能会使用相同的全局组件名,导致冲突。
  • 大量全局组件会增加项目打包的负担,也可能会引入不必要的依赖,增加项目打包后的体积。
  • 项目中使用了大量全局组件,不利于维护。

组件间的通信

组件间的通信有多种方法,以下列举几个常见的方式:

props传值(父传子)

props传值是有两个步骤:

  • 通过组件标签自定义属性传值。
  • 在子组件使用propsdefineProps声明要接收的值以及值的类型。

先看一下传值的是怎么使用的,如下:

<template>
    <HelloWord :username="name" />
</template>
<script setup>
    import { ref } from "vue";
    const name = ref("鞠婧祎");
</script>

子组件不使用语法糖的情况是通过props属性定义传值类型, 然后通过setup方法第一个参数接收传值。

<template>
    <div>Hello {{ name }}</div> 
</template>

<script>
    export default {
        props: {
            name: String // 定义传属性名为 name 并且是一个字符串类型
        },
        setup(props) { 
            // 在setup函数中第一个参数接收并使用传过来的值
            conso.lgo(props.name);
        }
    }
</script>

在语法糖写法中则使用内置的组合式APIdefineProps

<template>
    <div>Hello {{ name }}</div>
   <p>这里的模板插值直接使用name也可,前置的props模板会自动帮忙处理,当然也可以直接使用props.name</p>
</template>

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

    const props = defineProps({
        name: String // 声明要接收的值
    })
</script>

这里的模板插值直接使用name也可,前置的props在模板中会自动帮忙处理,当然也可以直接使用props.name

后面Vue3又将语法糖写法中defineProps这个API方法的导入帮我们也自动完成了,所以可以不用import导入可直接使用。

<template>
    <div>Hello {{ name }}</div>
</template>

<script setup>
    const props = defineProps({
        name: String // 声明要接收的值
    })
</script>

这种传值方式是使用得最多的,一般情况的需求都是父组件将值传给子组件即可,所以使用这方式的场景特别多。

使用emit事件通信(子传父)

通常情况下Vue是秉持着单向数据流的思想,如果是双向的话那组件的一多结构一复杂,对后续的维护是有一定负担的。所以一般情况下子组件是无法直接向父组件传值的。

但是既然直接传值不行,那我通过将传事件传到子组件,那么在子组件调用父组件的事件甚至在事件方法中还可以传递参数,这样一来就达到子传父的效果。

步骤如下:

  • 定义事件的方法
  • 通过在模板中使用@自定义事件名将事件传到子组件
  • 在子组件中使用emit属性声明要接收的事件
  • 在子组件中使用setup第二个参数组件上下文(ctx)来调用eimt来触发事件,emit方法使用格式为:ctx.emit(父组组件传来的事件名, 参数1,参数2, ...参数n)

父组件

<script setup>
    import { ref } from "vue";
    import HelloWord from "@/components/HelloWord.vue";

    const uname = ref("林允儿"); // 定义响应式变量

    const updateName = str => { // 定义事件方法,用于传给子组件
        uname.value = str;
    }
</script>

<template>
    <HelloWord :name="uname" @updatename="updateName"/> <!-- 自定义事件updatename,并将其传给子组件 -->
</template>

子组件

<template>
    <div>Hello {{ name }}</div> <!-- 显示父组件传过来的值 -->
    <button @click="handleBtn">修改</button>
</template>

<script>
    export default {
        setup(props, ctx) { //
            const handleBtn = () => {
                ctx.emit('updatename', '鞠婧祎') 
            }

            return { handleBtn };
        },
        props: {
            name: String // 声明要接收的值
        },
        emits: ['updatename'], // 声明要从父组件接收的事件
    }
</script>

在这个例子中,父组件传了一个name属性和一个updatename事件给了子组件,通过在子组件的button按钮事件中使用ctx.emit('updatename', '鞠婧祎'), 调用了父组件的传过来的方法,并身其传递了一个参数鞠婧祎过去,改变了父组件uname的值。

子组件语法糖写法:

<template>
    <div>Hello {{ name }}</div> <!-- 显示父组件传过来的传 -->
    <button @click="handleBtn">修改</button>
</template>

<script setup>
    import { defineEmits } from 'vue';

    const emits = defineEmits(['updatename']);
    const props = defineProps({
        name: String
    })

    const handleBtn = () => {
        emits('updatename','鞠婧祎');
    }
</script>

同样的defineEmitsdefineProps在语法糖写法中现在可以省略导入这一步骤了。

<template>
    <div>Hello {{ name }}</div> <!-- 显示父组件传过来的传 -->
    <button @click="handleBtn">修改</button>
</template>

<script setup>
    const emits = defineEmits(['updatename']);
    const props = defineProps({
        name: String
    })

    const handleBtn = () => {
        emits('updatename','鞠婧祎');
    }
</script>
自定义表单双向绑定

在前面的章节我们说过使用普通:value=来绑定表单的值只能是单向数据流绑定,并不能实现表单与响应式数据的双向数据流绑定,除非使用v-model才能达到效果。

其实v-model的本质就是使用了这种方式达到了在表单上更改从而能影响到响应式数据的效果,将input标签看成一个子组件,然后使用自带的input事件修改响应式数据的值,如下:

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

    const uname = ref("林允儿"); // 定义响应式变量

    const handleUpdateName = (e) => {
        uname.value = e.target.value; // 将表单的传输值赋值给响应式变量
    };
</script>

<template>
    <input type="text" :value="uname" @input="handleUpdateName">
    <div>{{ uname }}</div>
</template>

vue只是将这一过程形成了一个语法糖v-model的写法。

provideinject通信

已经讲过了父传子,子传父,那么祖传孙这种情况呢,如果按props的那种搞法,只能一级一级的祖传子,子传孙才能实现,这样未免太过麻烦了,所以就有了另一种provideinject这种方式。

使用步骤:

  • 在祖先组件中使用provide提供数据, 格式为: provide(提供的名称,提供者的值)
  • 在子或孙辈组件中使用inject将祖先提供的数据注入进来, 格式: inject(提供的名称 [, 默认值可不写])

我们先在components文件中创建新建两个组件,一个名为Parent.vue,一个名为Son.vue。 然后在App应用中使用Parent组件,在Parent组件中再使用Son组件,就形成了一个祖孙三代的组件结构。

App.vue

<script setup>
    import Parent from "./components/Parent.vue";
    import { provide, ref } from "vue"; // 导入provide方法

    const name = ref("林允儿"); // 声明响应式变量
    provide("name", name);
</script>

<template>
    <Parent/>
</template>

Parent.vue

<script setup>
    import Son from "@/components/Son.vue";
</script>

<template>
    <Son />
</template>

Son.vue

<script setup>
    import { inject } from "vue"; // 导入inject方法

    const myName = inject("name"); // 将值注入到myName中

</script>

<template>
    {{ myName }}
</template>
ref通信

尽管有了上面几种方便的组合互相通信的方式,还有一种方式就是在父组件可以直接操作子组件暴露出来的属性和方法,只是除非必要一般是不推荐使用的,原因还是上面说的在vue项目里最好还是遵循单向数据传输的原则,尤其在子组件中还暴露出许多属性和方法出来,对于多人协作和项目复杂起来维护起来的负担有点大。

在说这种方式之前先要介绍两个新知:一个是获取组件/元素的实例,另一个是向外暴露属性和方法.

  • 获取组件实例/元素Element实例

在模板中对组件标签和HTML标签使用ref属性可以将该组件/元素实例绑定到一个响应式变量。

<template>
    <div ref="myDiv">使用ref获取元素</div>
    <button @click="handleDiv">打印元素</button>
</template>
<script setup>
    import { ref, onMounted } from "vue";
    
    const myDiv = ref();
    
    console.log(myDiv.value); // 这时无法获取div元素,因为此时还在初使化中,组件还未被挂载。
    
    onMounted(() => {
        console.log(myDiv.value); // 当组件被挂载后,打印div元素
    });
    
    const handleDiv = () => {
        console.log(myDiv.value); // 可被打印,因为组件只有被挂载完毕了,button按钮才能显示出来。
    }
</script>
  • 暴露变量和方法到父组件

setup方法中使用expose暴露:

<script>
    import { ref } from "vue";
    export default {
        setup(props, { expose }) { // 从第二个参数ctx中解构出expose方法
            const name = ref("林允儿");
            
            const handleUpdateName = str => {
                name.value = str;
            }
            
            // 将name和handleUpdateName暴露给父组件
            expose({
                name,
                handleUpdateName
            });
        }
    }
</script>

语法糖写法:

<script setup>
    import { ref, defineExpose } from "vue";
    const name = ref("林允儿");
            
    const handleUpdateName = str => {
        name.value = str;
    }

    // 将name和handleUpdateName暴露给父组件
    defineExpose({
        name,
        handleUpdateName
    });
</script>

组合这两个就可以在父组件获取子组件实例,操作子组件暴露给父组件的属性和方法了,如:

父组件:

<script setup>
    import Parent from "./components/Test.vue";
    import { ref } from "vue"; 

    const elP = ref(); // 声明一个响应式变量用于获取模板元素

    const handleName = () => {
        elP.value.handleMsg("林允儿"); // 操作子组件暴露出来的handleMsg方法
        elP.value.id = 2; 操作子组件暴露出来的id属性
    }

</script>

<template>
    <Test ref="elP"/> <!--  使用ref属性绑定组件/元素,可以获取到组件/元素的实例 -->
    <button @click="handleName">Button</button> <!-- 用于触发修改子组件的按钮 -->
</template>

子组件:

<script setup>
    import { ref, defineExpose } from 'vue';

    const msg = ref("欢迎来到Vue");
    const id = ref(1);

    const handleMsg = str => {
        msg.value = str;
    }

    defineExpose({
        msg,
        handleMsg,
        id
    })
</script>

<template>
    <div>{{ msg }}</div>
</template>
$parent通信

这种方式和上面的有异曲同工之妙,只不过这回是子组件获取父组件的暴露出来的属性和方法。

父组件暴露属性和方法:

<script setup>
    import Parent from "./components/Parent.vue";
    import { ref, defineExpose } from "vue"; // 

    const msg = ref("欢迎学习vue3");

    const handleMsg = (str) => {
        msg.value = str;
    }

    defineExpose({
        msg, handleMsg // 暴露属性和方法到外部
    })

</script>

<template>
    <Parent :msg="msg"/>
</template>

在子组件中使用getCurrentInstance获取当前组件实例,然后再通过parent属性获取父组件实例

<script setup>
    import { ref, defineExpose, getCurrentInstance } from 'vue';

    const instance = getCurrentInstance(); // 获取当前组件实例
    const props = defineProps({
        msg: String
    })

    const handleBtn = () => {
        // instance.parent.ctx.$.exposed 获取父组件暴露出来的方法和属性
        instance.parent.ctx.$.exposed.handleMsg('这里不学习React!')
    }
</script>

<template>
    <div>{{ msg }}</div>
    <button @click="handleBtn">button</button>
</template>

同样这种方式也一般情况不用。

使用事件总线通信

这种方法的原理就是通过发布/订阅模式弄一个全局的事件调度平台供任何组件去使用,这种方法适合兄弟组件互相通信使用(没有共同的父/祖先组件),为了便于理解以下用一个最简单的发事件调度为例:

事件总线:

class Eventbus {
    constructor() {
        this.events = {}; // 存储订阅的事件
    }

    on(event, callback) { // 订阅事件
        if (!this.events[event]) {
            this.events[event] = [];
        }
        this.events[event].push(callback);
    }

    emit(event, data) { // 发布事件
        if (this.events[event]) {
            this.events[event].forEach(callback => callback(data));
        }
    }
}
const eventbus = new Eventbus();
export default eventbus;

组件1:

<script setup>
    import Parent from "./components/Parent.vue";
    import { ref } from "vue";
    import eventbus from "@/utils/eventbus.js"; // 导入调度器

    const msg = ref("欢迎学习vue3");

    const handleMsg = (str) => {
        msg.value = str;
    }

    eventbus.on("handleMsg", handleMsg); // 订阅一个handleMsg方法,用于更新msg的值

</script>

<template>
    <Parent :msg="msg"/>
</template>

组件2:

<script setup>
    import eventbus from "@/utils/eventbus.js"; // 导入事件调度器

    const props = defineProps({
        msg: String
    })

    const handleBtn = () => {
        eventbus.emit("handleMsg", "这里不是react!"); // 发布已订单的handleMsg方法,并传了一个参数,告诉调度器可以开始使用了
    }
</script>

<template>
    <div>{{ msg }}</div>
    <button @click="handleBtn">button</button>
</template>

如果不想自己实现这个调度器,可以使用现成的插件库如:mitt, 但是使用事件总线这种方式也是最好少使用,用得不好可能会有内存泄漏的风险,同时也不易于维护。

vuex或pinia

vuex/pinia这两个都是vue使用得最多的全局状态管理库,他们可以跨组件共享状态、方法,自然也就可以全局跨组件通信,因为内容比较多本章只是介绍一下,具体会在后面的单开一个章节讲这个。