跳至主要內容

Vue3

LincZero大约 13 分钟

Vue3

目录

通信方式

vue3组件通信方式

props

可以实现父子组件、子父组件、甚至兄弟组件通信

自定义事件

可以实现子父组件通信

全局事件总线$bus

可以实现任意组件通信

pubsub

发布订阅模式实现任意组件通信

vuex

集中式状态管理容器 实现任意组件通信

ref

父组件获取子组件实例VC 获取子组件的响应式数据以及方法

slot

插槽(默认插槽、具名插槽、作用域插槽)实现父子组件通信

props

props可以实现父子组件通信,在vue3中我们可以通过defineProps获取父组件传递的数据。且在组件内部不需要引入defineProps方法可以直接使用

父组件给子组件传递数据

Parent.vue

<template>
	<div class="parent">
        <h1>父组件</h1>
        <hr>
        <child info="hello" :money="money"></child>
    </div>
</template>

<script setup lang="ts">
import Child from './Child.vue'
import {ref} from 'vue'
let money = ref(10000)
</script>

Child.vue

<template>
	<div class="child">
        <h1>子组件</h1>
        <p>{{props.info}}</p>
        <p>{{props.money}}</p>
        <!-- 在这里props可以省略前面的名字,在script里则不行 -->
        <p>{{info}}</p>
        <p>{{money}}</p>
    </div>
</template>

<script setup lang="ts">
/**
1. defineProps是一个宏方法,setup也是语法糖,不需要引用
2. props是只读的,不可修改
3. `props`这个前面的名字在template中可以省略,在script中则不行
4. 这里的defineProps可以写成下面这种数组写法,或者可以写成对象写法
 */
let props = defineProps(['info', 'money']) 
</script>

<!--script lang="ts">
// Vue2 选项式的写法:
export default{
    props: ['info', 'money']
}
</script-->

自定义事件

Vue框架有两种事件:原生DOM事件、自定义事件

原生的DOM事件例如:

<template>
	<!-- vue2中这种写法就是自定义世界,可以通过.native修饰符变为原生DOM事件 -->
	<!-- vue3中这种写法就是原生的DOM事件 -->
	<pre @chick="handler1"></pre>
	<pre @chick="handler2(1,2,3,$event)"></pre>
</template>

<script setup lang="ts">
const handler1 = (event)=>{
    console.log(event);
}
const handler2 = (a,b,c,$event)=>{
    console.log(a,b,c,$event);
}
</script>

自定义事件:(可以实现组件之间的通信)

// 父组件
<template>
	<!-- 子组件给父组件传递 -->
	<Child @xxx="handler1"></Child>
</template>

<script setup lang="ts">
import Child from './Child.vue'
const handler1 = (param1, param2)=>{
    console.log(param1, param2); // 123, 456
}
</script>

// 子组件
<template>
	<buttom @click="handler"></buttom>>
</template>

<script setup lang="ts">
// defineEmits和defineProps类似,都是宏方法,不需要引用
let $emit = defineEmits(['xxx']);
cnst handler = ()=>{
    // 第一个参数是事件类型,后面的都是注入参数
    $emit('xxx', 123, 456);
}
</script>

全局事件总线(可兄弟通信)

全局事件总线可以实现任意组件通信,在vue2中可以根据VM与VC关系退出全局事件总线。 但在vue3中没有Vue构造函数,也就没有Vue.ptototype,以及组合式API写法没有this。 那么在Vue3想实现全局事件的总线功能就有点不现实啦,如果想在Vue3中使用全局事件总线功能可以使用插件mitt实现

mitt官网地址:https://www.npmjs.com/package/mitt(具体使用方法看这,这里面有代码示例,也可以在npm官网直接搜mitt)

这里是一个兄弟组件通信的demo,child2传递给child1:

import mitt form 'mitt';
const $bus = mitt(); // 该对象有all的Map和emit off on三个方法

// Child1.vue
<script setup lang="ts">
import $bus from '../../bus';
import {onMounted} from 'vue'; // 组合式API函数
// 组件挂载完毕后,当前组件绑定一个事件,接受将来兄弟组件传递的数据
onMounted(()=>{
    // @param1: 事件类型,@param2:事件回调
    $bus.on('car', (car)=>{ // 订阅
        console.log(car)
    })
})
</script>

// Child2.vue
<template>
	<div class="child2">
        <button @click="">
        	点击送给我兄弟一台法拉利    
    	</button>
    </div>
</template>
<script setup lang="ts">
import $bus from '../../bus';
    
const handler = ()=>{
    $bus.emit('car', {car: "法拉利"}) // 发送
}
</script>

v-model

除了可以修改表单数据外,数据双向绑定外。还可以进行父子组件的通信

双向绑定修改表单数据的用法:

<template>
	<div>
    	<input type="text" v-model="info">
    </div>
</template>
<script setup lang="ts">
import {ref} from 'vue';
let info = ref('');
</script>

父子组件通信以数据同步的用法:(旧方法,props+emit)

旧方法是父传递给子用props,子传递给父则用emit。原理是props数据只读不可改,那就通知父组件让父组件来改。

// Parent.vue
<template>
    <h3>钱数: {{money}}</h3>
	<Child :modelValue="money" @update:modelValue="handler"></Child>
</template>
<script setup lang="ts">
import {ref} from 'vue'
import Child from './Child.vue'
let money = ref(10000); // 父组件的数据
const handler = (num)=>{
	money.value = num;
}
</script>

// Child.vue
<template>
	<div class="child">
    	<h3>钱数: {{modelValue}}</h3>
        <button @click="handler">加钱</button>
    </div>
</template>
<script setup lang="ts">
import {ref} from 'vue';
let props = defineProps(['modelValue']);
let $emit = defineEmits(['update:modelValue']);
const handler = ()=>{
    $emit('update:modelValue', props.modelValue+1000);
}
</script>

父子组件通信以数据同步的用法:(新方法,v-model)

原理:本质是语法糖。相当于给子组件传递 props[modelValue]="10000" 并给子组件绑定自定义事件 update:modelValue (这两者都可以用VueTools验证,可以在浏览器中看到,子组件是多出了这两个东西的)

// Parent.vue
<template>
    <h3>钱数: {{money}}</h3>
	<Child v-model="money"></Child> <!--将这里修改成v-model就可以了-->
</template>
<script setup lang="ts">
import {ref} from 'vue'
import Child from './Child.vue'
let money = ref(10000); // 父组件的数据
</script>

// Child.vue
<template>
	<div class="child">
    	<h3>钱数: {{modelValue}}</h3>
        <button @click="handler">加钱</button>
    </div>
</template>
<script setup lang="ts">
import {ref} from 'vue';
let props = defineProps(['modelValue']);
</script>

可以绑定多个v-model

// 父组件
<template>
	<div>
        <Child2 v-model:pageNo="pageNo" v-model:pageSize="pageSize"></Child2> <!--相当于传了两个props-->
    </div>
</template>

// 子组件
<template>
	<div>
        <button @click="handler1">pageNo{{pageNo}}</button>
        <button @click="handler2">pageSize{{pageSize}}</button>
    </div>
</template>
<script setup lang="ts">
let props = defineProps(["pageNo", "pageSize"]);
let $emit = defineEmits(["update:pageNo", "update:pageSize"]);
const handler1 = ()=>{
	...
}
const handler2 = ()=>{
	...
}
</script>

useAttr方法

vue3提供一个useAttrs方法。其实和props差不多

// parent.vue
<template>
	<div>
        <el-button type="primary" size="small" :icon="Edit"></el-button>
        <!--自定义组件,封装一层el-button-->
        <HintButton type="primary" size="small" :icon="Edit" :title="提示"></HintButton>
    </div>
</template>

<script setup lang="ts">
import {
    Chick, Delete, Edit, Message, Search, Star
} from '@element-plus/icons-vue'
import HintButton from './HintButton.vue'
</script>

// HintButton.vue
<template>
	<div :title="title">
        <el-button :type="$attrs.type" :size="$attrs.small" :icon="$attrs.Edit"></el-button>
        <el-button :="$attrs"></el-button> <!--也可以这样写,是v-bind的语法糖,`:={a:1,b:2}`-->
    </div>
</template>

<script setup lang="ts">
// defineProps(['type', 'size', 'icon']); // 接收父组件传递过来数据
    
// 引入useAttrs方法,可以获取组件标签身上属性与事件
import {useAttr} from 'vue'
let $attrs = useAttrs();
    
let props = defineProps(["title"]); // 需要注意,如果用了props,attrs中就会没有对应的属性
</script>

props与useAttr方法都可以获取父组件传递过来的属性与属性值,但是props接受了useAttrs方法就获取不到了

ref、$parent与defineExpose

父组件使用子组件

// 父组件
<template>
	<div class="parent">
        <h1>父组件金额:{{money}}</h1>
        <button @click="handler">找儿子借100块</button>
        <Son ref="son"></Son>
    </div>
</template>
<script setup lang="ts">
// ref,可以获取真实DOM节点,可以获取到子组件实例VC
// $parent,可以在子组内部获取到父组件实例
import Son form './Son.vue'
import {ref} from 'vue';
let money = ref(100000000); // 父组件钱数
let son = ref(); // 子组件实例,必须要同名
const handler = () => {
    money.value += 10;
    son.value.money -= 10;
    son.value.fly();
}
</script>

// 子组件
<template>
	<h1>金额:{{money}}</h1>
</template>
<script setup lang="ts">
import {ref} from 'vue'
let money = ref(666); // 儿子钱数
const fly = () => {
    console.log("这是一个子组件方法");
}
defineExport({money, fly}); // 暴露给父亲。组件内部数据对外关闭,如果想让外部访问,需要通过defineExpose方法对外暴露
</script>

子组件使用父组件

// 父组件
<template>
	<div class="parent">
        <h1>父组件金额:{{money}}</h1>
        <Dau></Dau>
    </div>
</template>
<script setup lang="ts">
import Dau form './Daughter.vue'
import {ref} from 'vue';
let money = ref(100000); // 父组件钱数
defineExpose({money}); // 暴露给女儿
</script>

// 女组件
<template>
	<h1>金额:{{money}}</h1>
    <button @click="handler($parent)">找父亲借10000块</button>
</template>
<script setup lang="ts">
import {ref} from 'vue'
let money = ref(9999999999); // 女儿钱数
const handler = ($parent) => {
    money.value += 10000;
	$parent.money -= 10000;
}
</script>

provide与inject

// 父组件
<template>
	<div>
		<Child></Child>
    </div>
</template>

<script setup lang="ts">
import Child from './Child.vue'
// Vue3提供provide(提供)与inject(注入),可以实现隔辈组件传递数据
import {ref} from "vue"
let car = ref("法拉利");
    
// 祖先组件给后代组件提供数据
// 两个参数:
// 第一个参数就是提供数据的key
// 第二个参数是祖先提供的数据
import {provide} from "vue"
provide("TOKEN", car);
</script>

// 子组件
<template>
	<div>
		<GrandChild></GrandChild>
    </div>
</template>

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

// 孙组件
<template>
	<div>
        <h1>孙子 {{car}}</h1>
        <button @click="updateCar">换车</button>
    </div>
</template>

<script setup lang="ts">
// 注入祖先组件提供数据
// 需要参数:祖先提供数据的key
import {inject} from 'vue';
let car = inject('TOKEN');
const updateCar = ()=>{
    car.value = "自行车";
}
</script>

pinia

  • vuex
    • 集中式管理状态容器,可以实现任意组件之间通信
    • 核心概念:state、mutations、actions、getter、modules
  • pinia(翻译原因,也叫大菠萝)
    • 集中式管理状态容器,可以实现任意组件之间通信
    • 核心概念:state、actions、getters

需要另外 npm install

pinia有两种写法:选择式和组合式,选择式更类似于于vuex的写法

选择式API

// index.ts
// 创建大仓库
import {createPinia} from 'pinia';
let store = createPinia();
// 对外暴露,安装仓库
export default store;

// main.ts
import store from './store'
app.use(store); // 如果安装成功后,Vue开发者工具中,就会有一个Pinia的东西亮起

// info.ts
// 定义info小仓库。大仓库会自动管理小仓库
import {defineStore} from "pinia";
// 第一个仓库:小仓库名字 第二个参数:小仓库配置对象。defineStore方法执行会返回一个函数,函数作用是让组件可以获取到仓库数据
let useInfoStore = defineStore("info", {
	state:() => {
		return {
			count:99
		}
	},
	actions: {
		// 注意,函数没有context上下文对象
		updateNum(){
			this.count++;
		}
	},
	getters: {}
});
// 对外暴露方法
export default useInfoStore;

// 父组件
<template>
	<div>
    	<Child1></Child1>
        <Child2></Child2>
    </div>
</template>

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

// 子组件1
<template>
	<div class="child1">
    	{{infoStore.count}}
        <button @click="updateCount>点击我修改仓库数据</button>
    </div>
</template>

<script setup lang="ts">
import useInforStore form '../../store/modules/info'
// 获取小仓库对象
let infoStore = useInfoStore();
// 修改数据方法
const updateCount = ()=>{
    infoStore.count++; // 修改仓库数据的方法一
	infoStore.$patch({count:1111}); // 修改数据的第二种方法
    infoStore.updateNum // 也可以使用仓库的方法间接修改
}
</script>

// 子组件2
<template>
	<div class="child2">
    	
    </div>
</template>

<script setup lang="ts">
    
</script>

组合式API

插槽

Vuex

reactive

总结

GPT


所有对话将使用vue3+typescript的技术栈。

vue3中的通信方式有非常多种,通信对象包括父子、子父、兄弟、祖先等,通信方法包括:defineProps、自定义事件 defineEmits、全局事件总线、v-model、useAttr、ref $parent defineExpose、provide inject、pinia、插槽 这九种(如果有漏可以补充)。

分析这些通信方法各自的优缺点,以及选用策略。


以下是Vue 3 + TypeScript技术栈中各种通信方法的优缺点和使用策略:(共12种)

  • 父子组件通信
    • defineProps
      • 通信对象:父传递给子
      • 优点:类型安全,实现简单,是Vue 3官方推荐的属性传递方式。
      • 缺点:仅限于父子组件之间通信,缺乏灵活性。
      • 使用策略:在父子组件之间传递数据时使用。
    • 自定义事件(defineEmits)
      • 通信对象:子传递给父
      • 优点:类型安全,实现简单,Vue 3官方推荐使用自定义事件进行子组件通知父组件的操作。
      • 缺点:仅适用于子父组件通信,不能实现其他通信场景。
      • 使用策略:在子组件需要通知父组件发生某些事件时使用。
    • v-model 优点
      • 通信对象:父子互传(双向绑定)
      • 适合于表单元素的双向绑定,简化表单处理逻辑
      • 缺点:仅适用于表单场景,不能应对其他通信场景
      • 使用策略:在需要进行双向绑定场景中,特别是表单元素时使用。
    • 插槽(slots)
      • 通信对象:父传递给子
      • 优点:灵活性高,能将父组件内容放置于子组件任意位置
      • 缺点:仅在父子组件之间通信
      • 使用策略:在父子组件之间需要透传内容,或者自定义子组件结构时使用。
    • v-bind.sync
      • 通信对象:父子双向绑定
      • 优点:简化双向绑定代码,适用于非表单元素的双向绑定。
      • 缺点:Vue 3 不再推荐使用,使用场景较为有限。
    • reactive
      • 通信对象:父子双向绑定
      • 优点:保持父子组件间响应式对象的数据同步,适用于整个响应式对象需要传递给子组件的场景。
      • 缺点:需要手动监听变化并触发子组件的更新,相比其他通信方法较为繁琐。
      • 使用策略:如果需要将整个响应式对象传递给子组件,可以使用 reactive 方法。reactive 可以使对象在父子组件间保持响应式,但不会自动触发子组件的更新,需要手动监听变化并通知子组件。
  • 任意组件 / 兄弟组件通信
    • 全局事件总线
      • 通信对象:任意组件互传
      • 优点:能够跨组件通信,适用于各种通信场景,可以替代Vuex涉及的部分场景。
      • 缺点:缺乏类型安全,需要手动维护事件键名,在复杂的应用中容易引发问题。
      • 使用策略:在不同层级的组件需要通信时,无法使用父子通信方式时使用。
    • Pinia
      • 通信对象:任意组件互传
      • 优点:简化状态管理,易于集成,取代Vuex的部分功能。
      • 缺点:在简单应用中可能显得冗余。
      • 使用策略:在需要全局状态管理时使用,特别是应用规模不断扩大时适用。
    • Vuex
      • 通信对象:任意组件互传
      • 优点:适用于复杂状态管理,便于组织代码,良好的可测试性
      • 缺点:可能引入额外的复杂性,对于简单应用或许不必使用
  • 跨层级组件通信
    • provide / inject
      • 通信对象:父(祖先)传递给子(后代)
      • 优点:适用于跨层级组件通信,灵活性较高。
      • 缺点:类型安全性较差。
      • 使用策略:在祖先与后代组件之间需要通信时使用。
  • 高级通信技巧
    • useAttrs
      • 通信对象:父传递给子
      • 优点:能更方便地访问未在props中声明的属性,灵活处理属性传递
      • 缺点:类型安全性较差,使用时需要注意
      • 使用策略:在需要接收很多未在props中声明的属性时使用。
    • ref $parent defineExpose
      • 通信对象:父子互传
      • 优点:实现父子组件直接访问,减少传递层数
      • 缺点:紧耦合,影响组件的复用与维护。
      • 使用策略:在简单项目中,需要快速实现通信时适用,不推荐复杂项目中使用。

结论:在实际项目中,我们需要根据通信场景、组件关系和应用的复杂程度来选择合适的通信方法。官方推荐使用defineProps和自定义事件,因其具有更好的类型安全性和相对简单的实现逻辑。其他通信方法可以根据需要结合使用。