关于vue.js:Vue3-组件通信方式

64次阅读

共计 14735 个字符,预计需要花费 37 分钟才能阅读完成。

0. 前言

不论是 Vue2 还是 Vue3, 组件通信形式很重要,不论是我的项目还是面试都是常常用到的知识点。

回顾一下 Vue2 中组件的通信形式:

  • props: 能够实现父子组件、子父组件、甚至兄弟组件通信
  • 自定义事件: 能够实现子父组件通信
  • 全局事件总线 $bus: 能够实现任意组件通信
  • pubsub: 公布订阅模式实现任意组件通信
  • vuex: 集中式状态治理容器,实现任意组件通信
  • ref: 父组件获取子组件实例 VC, 获取子组件的响应式数据以及办法
  • slot: 插槽 (默认插槽、具名插槽、作用域插槽) 实现父子组件通信

示例代码地址:https://github.com/chenyl8848/vue-technology-stack-study

1. props

props 能够实现父子组件通信, 在 Vue3 中能够通过 defineProps 获取父组件传递的数据,且在组件外部不须要引入 defineProps 办法能够间接应用。

父组件给子组件传递数据

<template>
  <div class="box">
    <h1>props: 我是父组件 </h1>
    <Children :money = "money" :info = "info"></Children>
  </div>
</template>

<script lang="ts" setup>
import {ref} from "vue";
// 引入子组件
import Children from "./Children.vue";
// 应用 props 实现父子组件通信
let money = ref(1000);
let info = ref('发零花钱了')

</script>

<style scoped>
.box {
  width: 1000px;
  height: 500px;
  background-color: pink;
}
</style>

子组件获取父组件传递数据:

<template>
  <div>
    <h2>props: 我是子组件 </h2>
    <p> 接管父组件传值:{{props.money}}</p>
    <p> 接管父组件传值:{{props.info}}</p>
    <!-- 无奈批改 -->
    <!-- Set operation on key "money" failed: target is readonly. -->
    <!-- <button @click="updateProps"> 批改 props 的值 </button> -->
  </div>
</template>

<script lang="ts" setup>
// 须要应用到 defineProps 办法去承受父组件传递过去的数据
// defineProps 是 Vue3 提供办法, 不须要引入间接应用
// 数组 | 对象写法都能够
// let props = defineProps(["money", "info"]);
let props = defineProps({
  money: {
    // 接收数据的类型
    type: Number,
    default: 0,
  },
  info: {
    type: String,
    required: true,
  },
});

// props 是只读的,不能批改
// let updateProps = () => {
//   props.money = 3000;
// };
</script>

<style scoped>
</style>

留神:子组件获取到 props 数据就能够在模板中应用,然而切记 props 是只读的(只能读取,不能批改)。

2. 自定义事件

Vue 框架中事件分为两种:一种是原生的 DOM 事件,另外一种自定义事件。

原生 DOM 事件能够让用户与网页进行交互,比方 clickdbclickchangemouseentermouseleave

自定义事件能够实现子组件给父组件传递数据。

2.1 原生 DOM 事件

代码如下:

<pre @click="handler1"> 大江东去,浪淘尽 </pre>
<pre @click="handler2(1,2,3,$event)"> 千古风流人物 </pre>
let handler1 = (event) => {console.log(event)
}

let handler2 = (x, y, z, $event) => {console.log(x, y, z, $event)
}

pre 标签绑定原生 DOM 事件点击事件,默认会给事件回调注入 event 事件对象。当点击事件注入多个参数时,注入的事件对象务叫 $event.

Vue3 框架 clickdbclickchange(这类原生 DOM 事件), 不论是在标签、自定义标签上 (组件标签) 都是原生 DOM 事件。

Vue2 中却不是这样的, 在 Vue2 中组件标签须要通过 native 修饰符能力变为原生 DOM 事件。

2.2 自定义事件

自定义事件能够实现子组件给父组件传递数据。

在父组件外部给子组件绑定一个自定义事件:

<Children2 @xxx="handler4" @click="handler5"></Children2>

Children2 子组件外部触发这个自定义事件。

<template>
    <div>
        <h2> 自定义事件: 我是子组件 2 </h2>
        <button @click="handler"> 向父组件传值, 自定义事件 xxx</button>
        <br>
        <br>
        <button @click="$emit('click','321','world hello')"> 向父组件传值, 自定义事件 click</button>
    </div>
</template>

<script lang="ts" setup>
// 能够应用 defineEmits 返回函数触发自定义事件
// defineEmits 办法不须要引入间接应用

let $emit = defineEmits(['xxx', 'click']) 

let handler = () => {$emit('xxx', 123, 'hello world')
}
</script>

<style scoped>

</style>

defineEmitsVue3 提供的办法,不须要引入间接应用。defineEmits 办法执行,传递一个数组,数组元素即为未来组件须要触发的自定义事件类型,此方执行会返回一个 $emit 办法用于触发自定义事件。

当点击按钮的时候,事件回调外部调用 $emit 办法去触发自定义事件,第一个参数为触发事件类型,第二个、第三个、第 N 个参数即为传递给父组件的数据。

在父组件中接管子组件传递过去的参数:

let handler4 = (params1, params2) => {console.log(params1, params2)
}

let handler5 = (params1, params2) => {console.log(params1, params2)
}

3. 全局事件总线

全局事件总线能够实现任意组件通信,在 Vue2 中能够依据 VMVC 关系推出全局事件总线。

然而在 Vue3 中没有 Vue 构造函数,也就没有 Vue.prototype 以及组合式 API 写法没有 this,如果想在 Vue3 中应用全局事件总线性能,能够应用插件 mitt 实现。

mitt 官网地址:https://www.npmjs.com/package/mitt

3.1 mitt 装置

pnpm i mitt

3.2 mitt 定义

// 引入 mitt mitt 是一个办法,办法执行会返回 bus 对象
import mitt from 'mitt';

const $bus = mitt();

export default $bus;

3.3 mitt 应用

mitt 实现全局事件总线,实现兄弟组件之间进行通信:

<template>
  <div class="children2">
    <h2> 我是子组件 2 </h2>
    <button @click="handler"> 给兄弟组件传递值 </button>
  </div>
</template>

<script lang="ts" setup>
import $bus from '../../bus'

const handler = () => {$bus.emit('car', {car: '兰博基尼'})
}
</script>

<style scoped>
.children2 {
  width: 300px;
  height: 150px;
  background-color: yellowgreen;
}
</style>
<template>
  <div class="children1">
    <h2> 我是子组件 1 </h2>
  </div>
</template>

<script lang="ts" setup>
import $bus from "../../bus";

// console.log($bus)

// 应用组合式 API 函数
import {onMounted} from "vue";

// 组件挂载结束的时候,以后组件绑定一个事件,接管未来兄弟组件传递的数据
onMounted(() => {
  // 第一个参数即为事件类型 第二个参数即为事件回调
  $bus.on("car", (params) => {console.log("接管兄弟组件传值", params);
  });
});
</script>

<style scoped>
.children1 {
  width: 300px;
  height: 150px;
  background-color: yellow;
}
</style>

4. v-model

v-model 指令可是收集表单数据(数据双向绑定),除此之外它也能够实现父子组件数据同步。

v-model 理论时基于 props[modelValue] 与自定义事件 [update:modelValue] 实现的。

父组件:

<template>
  <div class="box">
    <h1> 我是父组件:v-model</h1>
    <input v-model="info" type="text" />

    <!-- 应用 props 向子组件传递数据 -->
    <!-- <Children1 :modelValue="info" @update:modelValue="handler"></Children1> -->

    <!-- 应用 v-model 向子组件传递数据 -->
    <!-- 
       在组件上应用 v-model
        第一: 相当有给子组件传递 props[modelValue]
        第二: 相当于给子组件绑定自定义事件 update:modelValue
     -->

    <div class="container">
      <Children1 v-model="info"></Children1>

      <Children2
        v-model:pageNo="pageNo"
        v-model:pageSize="pageSize"
      ></Children2>
    </div>
  </div>
</template>

<script lang="ts" setup>
import {ref} from "vue";
import Children1 from "./Children1.vue";
import Children2 from "./Children2.vue";

let info = ref("");
const handler = (params) => {info.value = params;};

let pageNo = ref(0);
let pageSize = ref(10);
</script>

<style scoped>
.box {
  width: 1000px;
  height: 500px;
  background: skyblue;
}

.container {
  display: flex;
  justify-content: space-between;
}
</style>

子组件 Children1

<template>
  <div class="children1">
    <h2> 我是子组件 1 </h2>
    <h2> 父组件 info 信息:{{props.modelValue}}</h2>
    <button @click="handler"> 同步更新父组件 info 信息 </button>
  </div>
</template>

<script lang="ts" setup>
// 应用 defineProps 接管父组件传值
let props = defineProps(["modelValue"]);
console.log(props);

let $emits = defineEmits(['update:modelValue'])

const handler = () => {$emits('update:modelValue', 'hello world')
}

</script>

<style scoped>
.children1 {
  width: 300px;
  height: 250px;
  background-color: yellow;
}
</style>

子组件 Children2

<template>
  <div class="children2">
    <h2> 我是子组件 2 </h2>
    <h3> 同时绑定多个 v -model</h3>
    <button @click="handler">pageNo: {{pageNo}}</button>
    <br />
    <br />
    <button @click="$emit('update:pageSize', pageSize + 10)">pageSize: {{pageSize}}</button>
  </div>
</template>

<script lang="ts" setup>

let props = defineProps(['pageNo', 'pageSize'])
let $emit = defineEmits(['update:pageNo', 'update:pageSize'])

const handler = () => {$emit('update:pageNo', props.pageNo + 1)
}

</script>

<style scoped>
.children2 {
  width: 300px;
  height: 250px;
  background-color: yellowgreen;
}
</style>

<Children1 v-model="info"></Children1> 相当于给组件 Children1 传递一个 props(modelValue) 与绑定一个自定义事件 update:modelValue 实现父子组件数据同步。

Vue3 中一个组件能够通过应用多个 v-model, 让父子组件多个数据同步, 下方代码相当于给组件 Children2 传递两个 props 别离是 pageNopageSize,以及绑定两个自定义事件 update:pageNoupdate:pageSize 实现父子数据同步。

<Children2 v-model:pageNo="pageNo" v-model:pageSize="pageSize"></Children2>

5. useAttrs

Vue3 中能够利用 useAttrs 办法获取组件的属性与事件(蕴含: 原生 DOM 事件或者自定义事件),该函数性能相似于 Vue2 框架中 attrs 属性与 $listeners 办法。

比方:在父组件外部应用一个子组件 HintButton

<template>
  <div class="box">
    <h1> 我是父组件:attrs</h1>
    <el-button type="primary" size="large" :icon="Edit"></el-button>
    <!-- 自定义组件 -->
    <!-- 实现将光标放在按钮上,会有文字提醒 -->
    <HintButton type="primary" size="large" :icon="Edit" @click="handler" @xxx="handler" :title="title"></HintButton>
  </div>
</template>

<script lang='ts' setup>
import {Edit} from "@element-plus/icons-vue";
import {ref} from "vue";
import HintButton from "./HintButton.vue";

const handler = () => {alert(12306)
}

let title = ref('编辑')

</script>
<style scoped>
.box {
  width: 1000px;
  height: 500px;
  background: skyblue;
}
</style>

子组件外部能够通过 useAttrs 办法获取组件属性与事件。它相似于 props, 能够承受父组件传递过去的属性与属性值。须要留神如果 defineProps 承受了某一个属性,useAttrs 办法返回的对象身上就没有相应属性与属性值。

<template>
  <div :title="title">
    <el-button :="$attrs"></el-button>
  </div>
</template>

<script lang='ts' setup>
// 引入 useAttrs 办法: 获取组件标签身上属性与事件
import {useAttrs} from "vue";
// 此办法执行会返回一个对象
let $attrs = useAttrs();

// 万一用 props 承受 title
let props = defineProps(['title'])
// props 与 useAttrs 办法都能够获取父组件传递过去的属性与属性值
// 然而 props 接管了 useAttrs 办法就获取不到了
console.log($attrs)

</script>
<style scoped>
</style>

6. ref$parent

ref 能够获取元素的 DOM 或者获取子组件实例的 VC。既然能够在父组件外部通过 ref 获取子组件实例 VC,那么子组件外部的办法与响应式数据父组件也是能够应用的。

比方: 在父组件挂载结束获取组件实例

父组件外部代码:

<template>
  <div class="box">
    <h1> 我是父组件:ref parent</h1>
    <h2> 父组件领有财产:{{money}}</h2>
    <button @click="handler"> 向子组件 1 拿 100 元 </button>
    <div class="container">
        <Children1 ref="children1"></Children1>
        <Children2 ref="children2"></Children2>
    </div>
  </div>
</template>


<script lang='ts' setup>
//ref: 能够获取实在的 DOM 节点, 能够获取到子组件实例 VC
//$parent: 能够在子组件外部获取到父组件的实例
import Children1 from './Children1.vue'
import Children2 from './Children2.vue'

import {ref} from 'vue'

let money = ref(10000)

// 获取子组件的实例
let children1 = ref()
let children2 = ref()
console.log(children1, children2)

// 父组件外部按钮点击回调
const handler = () => {
    money.value += 100
    children1.value.money -= 100
}

defineExpose({money})

</script>

<style scoped>

.box {
  width: 1000px;
  height: 500px;
  background: skyblue;
}

.container {
    display: flex;
    justify-content: space-between;
}
</style>

然而须要留神,如果想让父组件获取子组件的数据或者办法须要通过 defineExpose 对外裸露,因为 Vue3 中组件外部的数据对外“敞开的”,内部不能拜访。

<template>
  <div class="children1">
    <h2> 我是子组件 1 </h2>
    <h3> 子组件 1 领有财产:{{money}}</h3>
  </div>
</template>


<script lang='ts' setup>
import {ref} from "vue";

let money = ref(9999);

defineExpose({money})
</script>

<style scoped>
.children1 {
  width: 300px;
  height: 250px;
  background-color: yellow;
}
</style>

$parent 能够获取某一个组件的父组件实例 VC,因而能够应用父组件外部的数据与办法。必须子组件外部领有一个按钮点击时候获取父组件实例,当然父组件的数据与办法须要通过 defineExpose 办法对外裸露。

<template>
  <div class="children2">
    <h2> 我是子组件 2 </h2>
    <h3> 子组件 2 领有财产:{{money}}</h3>
    <button @click="handler($parent)"> 向父组件拿 300 元 </button>
  </div>
</template>

<script lang='ts' setup>
import {ref} from 'vue'

let money = ref(1000)

const handler = ($parent) => {
  money.value += 300
  console.log($parent)
  $parent.money -= 300
}
</script>

<style scoped>
.children2 {
  width: 300px;
  height: 250px;
  background-color: yellowgreen;
}
</style>

7. provideinject

Vue3 提供两个办法 provide[提供] 与 inject[注入],能够实现隔辈组件传递参数。

provide 办法用于提供数据,此办法执须要传递两个参数, 别离提供数据的 key 与提供数据 value.

<template>
  <div class="box">
    <h1> 我是父组件:provdide-inject</h1>
    <h1> 父组件领有汽车:{{car}}</h1>
    <Children></Children>
  </div>
</template>


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

//vue3 提供 provide(提供)与 inject(注入), 能够实现隔辈组件传递数据
import {provide, ref} from "vue";
let car = ref("保时捷 911");

// 先人组件给后辈组件提供数据
// 两个参数: 第一个参数就是提供的数据 key
// 第二个参数: 先人组件提供数据
provide("CAR", car);


</script>

<style scoped>
.box {
  width: 1000px;
  height: 500px;
  background: skyblue;
}
</style>

子组件代码:

<template>
  <div class="children">
    <h2> 我是子组件 </h2>
    <Grandson></Grandson>
  </div>
</template>


<script lang='ts' setup>
import Grandson from './Grandson.vue';

</script>

<style scoped>
.children {
  width: 500px;
  height: 250px;
  background: pink;
}
</style>

孙子组件能够通过 inject 办法获取数据, 通过 key 获取存储的数值

<template>
  <div class="grandson">
    <h3> 我是孙子组件 </h3>
    <p> 先人传下来的汽车:{{car}}</p>
    <button @click="handler"> 更换汽车 </button>
  </div>
</template>

<script lang='ts' setup>
import {inject} from "vue";

// 注入先人组件提供数据
// 须要参数: 即为先人提供数据的 key
let car = inject('CAR')

// 应用 provide-inject 通信能够批改数据
const handler = () => {car.value = '自行车'}

</script>

<style scoped>
.grandson {
  width: 200px;
  height: 200px;
  background: hotpink;
}
</style>

8. pinia

pinia 官网:https://pinia.web3doc.top/

pinia 也是集中式治理状态容器,相似于 Vuex. 然而外围概念没有 mutationmodules.

8.1 pinia 装置

pnpm i pinia

8.2 pinia 注册

创立 store

import {createPinia} from 'pinia';

let store = createPinia()

export default store;

main.ts 中注册应用:

import {createApp} from 'vue'

import store from './store'

import App from './App.vue'

const app = createApp(App)

app.use(store)

app.mount('#app')

8.3 pinia 应用

选项式 API 应用:

import {defineStore} from 'pinia'

// 第一个参数: 小仓库名字  第二个参数: 小仓库配置对象
//defineStore 办法执行会返回一个函数, 函数作用就是让组件能够获取到仓库数据
let useInfoStore = defineStore("info", {
    // 留神写法与 vue2 中 的 vuex 写法不同

    // state 存储数据
    state: () => {
        return {
            count: 999,
            arr: [1, 2, 3, 4, 5, 6, 7, 8, 9]
        }
    },
    // 对数据进行操作
    actions: {
        // 留神: 函数没有 context 上下文对象
        // 没有 commit、没有 mutations 去批改数据
        updateCount(param1: number, param2: number) {this.count = param1 + param2}
    },
    // 获取数据
    getters: {total() {let result:number = this.arr.reduce((prev, next) => {return prev + next}, 0)
            return result
        }
    }
})

export default useInfoStore;

在组件中应用:

<template>
  <div class="children1">
    <h2> 我是子组件 1 </h2>
    <h3>count:{{infoStore.count}}</h3>
    <button @click="handler"> 批改 infoStore 中的数据 </button>
    <h3>total:{{infoStore.total}}</h3>
  </div>
</template>


<script lang='ts' setup>
import useInfoStore from "../../store/module/info";

// 获取小仓库对象
let infoStore = useInfoStore()

const handler = () => {
  // 仓库调用本身的办法去批改仓库的数据
  infoStore.updateCount(99, 100)
}

</script>

<style scoped>
.children1 {
  width: 300px;
  height: 250px;
  background-color: yellow;
}
</style>

组合式 API 应用:

import {defineStore} from 'pinia'
import {computed, ref} from 'vue'

let useTodoStore = defineStore('todo', () => {
    let todoList = ref([{ id: 1, title: '吃饭', done: true},
        {id: 2, title: '睡觉', done: false},
        {id: 3, title: '打游戏', done: true}
    ])

    let arr = ref([1, 2, 3, 4, 5])

    const total:any = computed(() => {return arr.value.reduce((prev, next) => {return prev + next}, 0)
    })

    const updateTodo = (params: any) => {todoList.value.unshift(params)
    }

    // 务必要返回一个对象: 属性与办法能够提供给组件应用
    return {
        todoList,
        total,
        updateTodo
    }
})

export default useTodoStore;

在组件中应用:

<template>
  <div class="children2">
    <h2> 我是子组件 2 </h2>
    <ul>
      <li v-for="(item, index) in todoStore.todoList" :key="item.id">{{item.title}}</li>
    </ul>
    <button @click="handler"> 批改 todo</button>
    <h3>totalCount:{{todoStore.total}}</h3>
  </div>
</template>

<script lang='ts' setup>
import useTodoStore from '../../store/module/todo'

let todoStore = useTodoStore()

const handler = () => {let params = { id: 4, title: '学习', done: true}
  todoStore.updateTodo(params)
}
</script>

<style scoped>
.children2 {
  width: 300px;
  height: 250px;
  background-color: yellowgreen;
}
</style>

9. slot

插槽:默认插槽、具名插槽、作用域插槽能够实现父子组件通信。

9.1 默认插槽

在子组件外部的模板中书写 slot 全局组件标签

<template>
  <div class="children1">
    <h2> 我是子组件 1 </h2>
    <!-- 默认插槽 -->
    <slot></slot>
  </div>
</template>

<script lang='ts' setup>

</script>

<style scoped>

</style>

在父组件外部提供构造,Children1 即为子组件,在父组件外部应用的时候,在双标签外部书写构造传递给子组件。

<template>
  <div class="box">
    <h1> 我是父组件:slot</h1>
    <div class="container">
      <Children1>
        <span> 默认插槽 </span>
      </Children1>
    </div>
  </div>
</template>

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

</script>

<style scoped>

</style>

留神开发我的项目的时候默认插槽个别只有一个。

9.2 具名插槽

顾名思义,此插槽带有名字,在组件外部能够有多个指定名字的插槽。

上面是一个子组件外部,模板中有两个插槽:

<template>
  <div class="children1">
    <h2> 我是子组件 1 </h2>
    <!-- 默认插槽 -->
    <slot></slot>
    <slot name="a"></slot>
    <slot name="b"></slot>
  </div>
</template>

<script lang='ts' setup>

</script>

<style scoped>
.children1 {
  width: 300px;
  height: 250px;
  background-color: yellow;
}
</style>

父组件外部向指定的具名插槽传递构造,v-slot 能够替换为 #

<template>
  <div class="box">
    <h1> 我是父组件:slot</h1>
    <div class="container">
      <Children1>
        <span> 默认插槽 </span>
      </Children1>
      <Children1>
        <template v-slot:a>
          <span> 具名插槽 a </span>
        </template>
      </Children1>
      <Children1>
        <template #b>
          <span> 具名插槽 b </span>
        </template>
      </Children1>
    </div>
  </div>
</template>


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

</script>

<style scoped>
.box {
  width: 1000px;
  height: 500px;
  background: skyblue;
}

.container {
  display: flex;
  justify-content: space-between;
}
</style>

9.3 作用域插槽

作用域插槽:子组件数据由父组件提供,然而子组件外部决定不了本身构造与外观(款式)

子组件 Children2 代码如下:

<template>
  <div class="children2">
    <h2> 我是子组件 2: 作用域插槽 </h2>
    <ul>
      <li v-for="(item, index) in todos" :key="item.id">
        <!-- 作用域插槽: 能够讲数据回传给父组件 -->
        <slot :$row="item" :$index="index"></slot>
      </li>
    </ul>
  </div>
</template>

<script lang='ts' setup>
// 通过 props 承受父组件传递数据
defineProps(["todos"]);
</script>

<style scoped>
.children2 {
  width: 300px;
  height: 250px;
  background-color: yellowgreen;
}
</style>

父组件外部代码如下:

<template>
  <div class="box">
    <h1> 我是父组件:slot</h1>
    <div class="container">
      <Children1>
        <span> 默认插槽 </span>
      </Children1>
      <Children1>
        <template v-slot:a>
          <span> 具名插槽 a </span>
        </template>
      </Children1>
      <Children1>
        <template #b>
          <span> 具名插槽 b </span>
        </template>
      </Children1>
      <Children2 :todos="todos">
        <template v-slot="{$row, $index}">
          <p :style="{color: $row.done ?'green':'red'}">
            {{$row.title}}
          </p>
        </template>
      </Children2>
    </div>
  </div>
</template>


<script lang='ts' setup>
import Children1 from "./Children1.vue";
import Children2 from "./Children2.vue";

// 插槽: 默认插槽、具名插槽、作用域插槽
// 作用域插槽: 就是能够传递数据的插槽, 子组件能够将数据回传给父组件, 父组件能够决定这些回传的数据是以何种构造或者外观在子组件外部去展现!!!import {ref} from "vue";
//todos 数据
let todos = ref([{ id: 1, title: "吃饭", done: true},
  {id: 2, title: "睡觉", done: false},
  {id: 3, title: "打游戏", done: true},
  {id: 4, title: "学习", done: false},
]);
</script>

<style scoped>
.box {
  width: 1000px;
  height: 500px;
  background: skyblue;
}

.container {
  display: flex;
  justify-content: space-between;
}
</style>

正文完
 0