Vue3
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 可以使对象在父子组件间保持响应式,但不会自动触发子组件的更新,需要手动监听变化并通知子组件。
- defineProps
- 任意组件 / 兄弟组件通信
- 全局事件总线
- 通信对象:任意组件互传
- 优点:能够跨组件通信,适用于各种通信场景,可以替代Vuex涉及的部分场景。
- 缺点:缺乏类型安全,需要手动维护事件键名,在复杂的应用中容易引发问题。
- 使用策略:在不同层级的组件需要通信时,无法使用父子通信方式时使用。
- Pinia
- 通信对象:任意组件互传
- 优点:简化状态管理,易于集成,取代Vuex的部分功能。
- 缺点:在简单应用中可能显得冗余。
- 使用策略:在需要全局状态管理时使用,特别是应用规模不断扩大时适用。
- Vuex
- 通信对象:任意组件互传
- 优点:适用于复杂状态管理,便于组织代码,良好的可测试性
- 缺点:可能引入额外的复杂性,对于简单应用或许不必使用
- 全局事件总线
- 跨层级组件通信
- provide / inject
- 通信对象:父(祖先)传递给子(后代)
- 优点:适用于跨层级组件通信,灵活性较高。
- 缺点:类型安全性较差。
- 使用策略:在祖先与后代组件之间需要通信时使用。
- provide / inject
- 高级通信技巧
- useAttrs
- 通信对象:父传递给子
- 优点:能更方便地访问未在props中声明的属性,灵活处理属性传递
- 缺点:类型安全性较差,使用时需要注意
- 使用策略:在需要接收很多未在props中声明的属性时使用。
- ref $parent defineExpose
- 通信对象:父子互传
- 优点:实现父子组件直接访问,减少传递层数
- 缺点:紧耦合,影响组件的复用与维护。
- 使用策略:在简单项目中,需要快速实现通信时适用,不推荐复杂项目中使用。
- useAttrs
结论:在实际项目中,我们需要根据通信场景、组件关系和应用的复杂程度来选择合适的通信方法。官方推荐使用defineProps和自定义事件,因其具有更好的类型安全性和相对简单的实现逻辑。其他通信方法可以根据需要结合使用。