Vue2、3组件通信的方式

都2025年了,为什么还要写一篇文章专门介绍Vue的组件通信呢?正如我其它文章里面提到的,前端工程化的前提就是合理的组件规划,组件的编写一定要遵守高内聚、低耦合,还有很重要的开闭原则,组件划分的时候一定要考虑后期的功能扩展、功能变动 。如果你划分的组件,在功能需求发生改变或者后期功能扩展的时候让人根本无从下手,那你就要反思一下你自己的专业性。这篇文章除了介绍Vue官方提供的一些常规组件通信方式之外,还会介绍一个很抽象的概念,传递组件

一、官方提供的组件通信方式

1、props传值

最常用的一种方式,这种方式你可以传递任何数据类型,string, number, boolean, array, object, function

比较取巧的一种方式是,你可以传递一个函数,使得子组件可以改父组件的变量,同时向父组件传递数据

html 复制代码
// parent
<template>
  <ChildCom :delivery-data="receiveDataFromChild"/>
</template>

<script setup lang="ts">
import ChildCom from './ChildCom.vue'

const receiveDataFromChild = (data: object) => {
  console.log('接收到了子组件的data', data);
  
}
</script>

// child
<template>
  <h2>child</h2>
</template>

<script setup lang="ts">
const props = defineProps<{
  deliveryData: (data: object) => void
}>()

props.deliveryData({ hello: 'this is message from child component' })
</script>

2、emit通信

官方提供的向父组件传递数据的方式

html 复制代码
<template>
  <ChildCom @get-data="receiveDataFromChild"/>
</template>

<script setup lang="ts">
import ChildCom from './ChildCom.vue'

const receiveDataFromChild = (data: object) => {
  console.log('接收到了子组件的data', data);
}
</script>

<template>
  <h2>child</h2>
</template>

<script setup lang="ts">
const emit = defineEmits<{
  getData: [{ name: string }]
}>()

emit('getData', { name: 'child' })
</script>

3、Vue2的$bus

全局事件总线,需要监听和发起

4、Vue3的provide/inject

可以在有父子关系的组件中跨组件通信,如果你想传递一个不能被子组件修改的值的话,有很多方式,比如传递计算属性、仅仅传递一个getter函数

js 复制代码
// main.js
import { provide } from 'vue'

provide('appName', 'My App')

const obj = {
  name: 'zhangsan'
}

const getObjName = () => {
  return obj.name
}

provide('getObjName', getObjName)
html 复制代码
<script setup lang="ts">
// 项目的任何页面
import { inject } from 'vue';

const appName = inject('appName');

const objName = inject('getObjName') as () => string

console.log(appName, objName())
</script>

5、pinia

官方推荐的替代vuex的状态管理库,使用更方便,数据也更清晰

6、作用域插槽

作用域插槽写到这里,不仅仅是因为默认插槽、具名插槽可以向父组件传递组件信息的同时不丢失响应性,还因为,作用域插槽可以有emit函数的功能,向父组件暴露子组件的变量。

html 复制代码
<template>
  <ul>
    <li v-for="item in studentsArr" :key="item.name">
      <slot name="header" :student="item">
        {{ item.name }} - {{ item.age }} - {{ item.class }}
      </slot>
    </li>
  </ul>
</template>

<script setup lang="ts">
const studentsArr = [
  {
    name: 'zhangsan',
    age: 18,
    class: 1
  },
  {
    name: 'lisi',
    age: 19,
    class: 1
  },
]
</script>

```html
<template>
  <ChildCom>
    <template #header="{ student }">
      <span>姓名:{{ student.name }},年龄:{{ student.age }},班级:{{ student.class }}</span>
    </template>
  </ChildCom>
</template>

<script setup lang="ts">
import ChildCom from './ChildCom.vue'
</script>

二、逆向插槽 ------ 子组件向父组件传递组件

如果你的组件划分是合理的话,上述方式已经完完全全足够你开发任何项目了,但是,你自己合规,并不能代表接手的别人的代码也合规。遇到过一个场景,之前的开发他也会组件划分,但是划分方式极不合理,后期功能扩展的时候完全无法下手,由此引发了我对组件传值的思考。下文将完整介绍我的思路发展过程。示例代码只简单的描述组件关系。项目其实使用的是ts,但是为了降低一次性的信息量,案例使用js展示

html 复制代码
<template>
    <!-- 布局组件layout-com.vue -->
    <CardCom title="现在它只支持传入文字">
        <!-- 会有很多个业务组件,这里只写一个用作示例 -->
        <BusinessCom/>
        <BusinessCom1/>
        <BusinessCom2/>
        <BusinessCom3/>
    </CardCom>
</template>

<script setup name="LayoutCom">
import CardCom from './card-com.vue'
import BusinessCom from './bussiness.vue'
</script>
html 复制代码
<template>
    <!-- 卡片布局组件card-com.vue -->
    <div class="card-wp">
        <div class="card-title-wp">
            <div class="card-title-left">
                <h3>{{ title }}</h3>
            </div>
            <p class="card-title-right">title-right</p>
        </div>
        <div>
            <p>这里展示内容</p>
            <!-- 默认插槽显示位置 -->
            <slot></slot>
        </div>
    </div>
</template>

<script setup name="CardCom">
defineProps({
    title: {
        type: String,
        default: ''
    }
})
</script>
html 复制代码
<template>
    <!-- 业务组件business-com.vue -->
    <button @click="handleClick"> 点这个按钮num++ </button>
    <p>{{ num }}</p>
</template>

<script setup name="BusinessCom">
const num = ref(0)

const handleClick = () => {
    // 仅做展示用,实际业务场景比这复杂的多
    num.value += 1
}
</script>

上边展示了现有组件之间的关系,新需求是,将业务组件的部分操作移动到卡片的标题右边,也就是card-title-left的div里面。

很明显,就像我之前提到的,如果组件划分、使用的方式合理的话,官方提供的那些通信方式完完全全够用,比如,像下边这样。

html 复制代码
<template>
    <!-- 布局组件layout-com.vue -->
    <CardCom title="现在它只支持传入文字">
        <BusinessCom/>
    </CardCom>
    
    <CardCom title="现在它只支持传入文字">
        <BusinessCom/>
    </CardCom>
    
    <CardCom title="现在它只支持传入文字">
        <BusinessCom/>
    </CardCom>
</template>

<script setup name="LayoutCom">
import CardCom from './card-com.vue'
import BusinessCom from './bussiness.vue'
</script>

如果是这种方式的话,很简单,在CardCom里面加一个具名插槽 就可以了,但是,聪明的前辈程序员不是这么写的,它现在只对这个组件用了一次,而且,基于原有的组件方式,已经实现了很复杂的交互场景,要想改的话,当前业务场景相当于完全重写。

所以,如果是你们,你们现在打算怎么做?

好的,重点来了,现在介绍一下我的解决方案

  • 首先,我们都知道javascript的特点,万物皆对象,任何参数其实都可以通过函数参数的方式传递,而vue是一个js框架,那么,我们是不是可以通过参数的方式传递vue组件?
  • 好的,那么,让我们打印一下vue引入的组件,看看在控制台会输出什么?
  • 它是不是个对象?那么,我们是不是就可以通过参数的方式传递这个组件?试一下,看看代码能不能运行
html 复制代码
<template>
    <!-- 布局组件layout-com.vue -->
    <CardCom title="现在它只支持传入文字">
        <template #title-operate>
            <!-- 必须加这个判断,不然不符合业务规则 -->
            <component v-if="deliveryCom" :is="deliveryCom"/>
        </template>
        <!-- 会有很多个业务组件,这里只写一个用作示例 -->
        <BusinessCom @delivery="transferComponent" />
        <BusinessCom1 />
        <BusinessCom2 />
        <BusinessCom3 />
    </CardCom>
</template>

<script setup name="LayoutCom">
import CardCom from './card-com.vue'
import BusinessCom from './bussiness.vue'

const deliverCom = shallowRef(null)

const transferComponent = (params) => {
    if(params.componentName === "BussinessCom") {
        deliverCom.value = params.component
    }
}
</script>
html 复制代码
<template>
    <!-- 卡片布局组件card-com.vue -->
    <div class="card-wp">
        <div class="card-title-wp">
            <div class="card-title-left">
                <h3>{{ title }}</h3>
                
                <!-- 新加的一行代码 -->
                <slot name="titleOperate"/>
            </div>
            <p class="card-title-right">title-right</p>
        </div>
        <div>
            <p>这里展示内容</p>
            <!-- 默认插槽显示位置 -->
            <slot></slot>
        </div>
    </div>
</template>

<script setup name="CardCom">
defineProps({
    title: {
        type: String,
        default: ''
    }
})
</script>
html 复制代码
<template>
    <!-- vue3 -->
    <!-- 业务组件business-com.vue -->
    <!-- <button @click="handleClick"> 点这个按钮num++ </button> -->
    <p>{{ num }}</p>
</template>

<script setup lang="jsx" name="BusinessCom">

defineEmits([''])
const num = ref(0)

const handleClick = () => {
    // 仅做展示用,实际业务场景比这复杂的多
    num.value += 1
}

const deliveryTest = defineComponent({
    render() {
        return <button onClick={handleClick}> 点这个按钮num++ </button>
    }
})

onMounted(()=>{
    emit('delivery', {
        componentName: 'BussinessCom',
        component: deliveryTest
    })
})
</script>
html 复制代码
<template>
	<!-- vue2 -->
  <!-- 业务组件business-com.vue -->
  <!-- <button @click="handleClick"> 点这个按钮num++ </button> -->
  <p>{{ num }}</p>
</template>

<script>
export default {
  data() {
    return {
      num: 0,
    };
  },
  mounted() {
    this.$emit("delivery", {
      componentName: "BussinessCom",
      component: {
        render: () => {
          return <button onClick={this.handleClick}> 点这个按钮num++ </button>;
        },
      },
    });
  },
  methods: {
    handleClick() {
      this.num++;
    },
  },
};
</script>

完美运行,响应式未丢失,而且,原有的通信的方式不再局限于js本身的数据格式,而且实现了逆向插槽 ,即组件的子组件向父组件传递。虽然每次增加一个组件都需要新定义一个变量,但是,这是为了修补别人的代码做出的替代方案,毕竟,一个合格的程序员不会写出这种离谱的结构。

上面的这种方式还用到的jsx语法,不会的同学可以去了解一下,这种方式使得Vue本身其实也具有巨大的灵活性,Vue模板本身就是前端编程界的一个壮举,再加入jsx,使得Vue具有了react的灵活性。同时,还有它强大的响应式系统和本身就做了的组件优化,这俩都是react相对的弱项。

相关推荐
栀一一几秒前
@click和@click.stop的区别
vue.js
Midsummer7 分钟前
nuxt安装报错-网络问题
vue.js·nuxt.js
张童瑶7 分钟前
Vue Electron 使用来给若依系统打包成exe程序,出现登录成功但是不跳转页面(已解决)
javascript·vue.js·electron
旺仔牛仔QQ糖33 分钟前
Vue为普通函数添加防抖功能(基于Pinia 插件为action 配置防抖功能 引发思考)
前端·vue.js
*小雪43 分钟前
vue2使用vue-cli脚手架搭建打包加密方法-JavaScript obfuscator
前端·javascript·vue.js
用户3802258598241 小时前
前端防篡改水印
vue.js
hnlucky2 小时前
安装vue的教程——Windows Node.js Vue项目搭建
前端·javascript·vue.js·windows·node.js
我的div丢了肿么办2 小时前
ResizeObserver和IntersectionObserver的详细讲解
前端·javascript·vue.js
JunjunZ2 小时前
ElementUI Tree组件的父子节点联动实现
vue.js·element
修仙的人3 小时前
老板:让 AI 大模型输出📊图表&🗺️地图
前端·vue.js·llm