学习vue3总是绕不开vue2
vue2组件通信方式总结:
1、props:可以实现父子组件,子父组件,甚至兄弟组件通信
2、自定义事件:实现子父组件通信
3、全局事件总线$bus:可以实现任意组件通信
4、pubsub:发布订阅模式实现任意组件通信
5、vuex: 集中式状态管理容器,实现任意组件通信
6、ref:父组件获取子组件实例VC,获取子组件的响应式数据及方法
7、slot:插槽(默认插槽,具名插槽,作用域插槽),实现父子组件通信
vue3组件通信方式总结:
1、props
2、自定义事件
3、全局事件总线
4、v-model
5、useAttrs方法
6、ref与$parent
7、provide与inject
8、pinia组合式API
9、插槽
vue3组件通信方式如下:
1、父子组件通信props
props数据只读,从父组件传递到子组件,子组件内部不可直接更改
子组件需要使用defineProps方法接受父组件传递过来的数据
父组件
<template>
<div class="box">
<Child :msg="msg"></Child>
</div>
</template>
<script setup lang="ts">
import Child from "./Child.vue";
import { ref } from "vue";
let msg= ref(10000);
</script>
子组件
<template>
<div class="son">
<p>{{msg}}</p>
</div>
</template>
<script setup lang="ts">
let props = defineProps(['msg']); //数组|对象写法都可以
</script>
2、自定义事件传值
vue框架中事件分为两种:原生的DOM事件和自定义的事件,原始DOM事件可以让用户与网页进行交互,如:click,dbclick,mouseenter,mouseleave等
自定义事件可以实现子组件给父组件传递数据
如果在父组件的组件标签上直接使用原生DOM事件,在vue2中可以通过.native修饰符变为原生DOM事件,在vue3中就是原生事件,相当于给子组件的根节点绑定的事件,子组件中的任意内容都可以触发这个事件。
<Event1 @click="handler2"></Event1>
vue3中没有this,没有组件实例,利用defineEmits方法返回函数触发自定义事件
父组件
<template>
<div>
<!-- 绑定自定义事件xxx:实现子组件给父组件传递数据 -->
<Event2 @xxx="handler3" @click="handler4"></Event2>
</div>
</template>
<script setup lang="ts">
//引入子组件
import Event2 from './Event2.vue';
const handler3 = (param1,param2)=>{
console.log(param1,param2);
}
const handler4 = (param1,param2)=>{
console.log(param1,param2);
}
</script>
子组件
<template>
<div class="child">
<p>我是子组件2</p>
<button @click="handler">点击我触发自定义事件xxx</button>
//直接在模板区触发,当在子组件中定义事件名为click,在父组件中即为自定义事件,不再是原生DOM事件
<button @click="$emit('click','AK47','J20')">点击我触发自定义事件click</button>
</div>
</template>
<script setup lang="ts">
let $emit = defineEmits(['xxx','click']);
//按钮点击回调
const handler = () => {
//第一个参数:事件类型 第二个|三个|N参数即为注入数据
$emit('xxx','东风导弹','航母');
};
</script>
3、兄弟组件传值
vue3框架中,没有vue构造函数,导致没有VM,也没有原型对象,以及组合式API写法setup没有this,可以用插件mitt实现全局事件总线功能。
mitt官网地址
npm install --save mitt
src目录下新建bus文件夹–index.ts文件中
//引入mitt插件:mitt一个方法,方法执行会返回bus对象
import mitt from 'mitt';
const $bus = mitt();
export default $bus;
emit触发事件,on接收参数
子组件1
import $bus from "../../bus";
import { onMounted } from "vue";
//组件挂载完毕的时候,当前组件绑定一个事件,接受将来兄弟组件传递的数据
onMounted(() => {
//第一个参数:即为事件类型 第二个参数:即为事件回调
$bus.on("car", (car) => {
console.log(car);
});
});
子组件2
<template>
<div class="child2">
<button @click="handler">赠送车</button>
</div>
</template>
<script setup lang="ts">
//引入$bus对象
import $bus from '../../bus';
//点击按钮回调
const handler = ()=>{
$bus.emit('car',{car:"法拉利"});
}
</script>
4、v-model父子组件通信
vue2中不可以进行父子组件通信,vue3中相当于同时做了props和emit的工作
5、useAttrs方法
useAttrs方法可以获取组件标签身上的属性与事件
如果同时使用props,props的优先级比useAttrs高。props接受了useAttrs方法就获取不到了
useAttrs方法不仅可以拿到组件标签身上的原生事件,自定义事件也可以拿到
父组件,父组件不需要向子组件传值,组件标签上的type,size,icon,原生事件及自定义事件都可以通过useAttrs方法获取到
<template>
<div>
<!-- 自定义组件 -->
<HintButton type="primary" size="small" :icon="Edit" title="编辑按钮" @click="handler" @xxx="handler"></HintButton>
</div>
</template>
子组件
<template>
<div :title="title">
//对象格式:{type:'primary',size:'small',icon:'Edit',title:'编辑按钮'}
<el-button :="$attrs"></el-button>
</div>
</template>
//引入useAttrs方法:获取组件标签身上属性与事件
import {useAttrs} from 'vue';
//此方法执行会返回一个对象
let $attrs = useAttrs();
如果用props接收title
let props =defineProps(['title']);
props接受了useAttrs方法就获取不到了
原生事件及自定义事件都可以通过useAttrs方法获取到
6、ref与$parent
ref获取到真实的DOM节点,可以获取到子组件的实例VC
$parent可以在子组件的内部获取到父组件的实例
1、ref
在父组件通过ref获取子组件的实例
<template>
<div class="box">
<Son ref="son"></Son>
</div>
</template>
import {ref} from 'vue';
let money = ref(100000000);
//获取子组件的实例,与组件标签上的ref同名,不然拿不到实例
let son = ref();
拿到子组件的实例后,组件内部数据对外是关闭的,别人不能访问,想让外部访问需要通过defineExpose方法对外暴露
子组件中
import {ref} from 'vue';
//子组件钱数
let money = ref(666);
defineExpose({
money,
})
暴露之后,就可以在父组件改子组件的数据了
父组件
<button @click="handler">修改</button>
//父组件内部按钮点击回调
const handler = ()=>{
//子组件钱数减去10
son.value.money-=10;
}
2、$parent
在子组件内部,点击事件的参数中传入 $parent,(固定写法)
子组件
<template>
<div class="dau">
<button @click="handler($parent)">点击我</button>
</div>
</template>
import {ref} from 'vue';
//子组件钱数
let money = ref(999999);
//子组件按钮点击回调
const handler = ($parent)=>{
money.value+=10000;
$parent.money-=10000;
}
打印$parent
没有拿到父组件的数据,需要在父组件也对外暴露一下
父组件
//对外暴露
defineExpose({
money
})
此时再打印,就有数据了
7、provide与inject
可以实现隔辈组件的通信,provide(提供),inject(注入)
1、祖先组件:祖先组件给后代组件提供数据
import { ref, provide } from "vue";
let car = ref("法拉利");
provide 方法有两个参数,key以及value
//两个参数:第一个参数就是提供的数据key
//第二个参数:祖先组件提供数据
provide("getData", car);
2、后辈组件,子组件/孙子组件都可以使用并且可以修改值
<button @click="updateCar">更新数据</button>
import {inject} from 'vue';
//注入祖先组件提供数据
//需要参数:即为祖先提供数据的key
let car = inject('getData');
const updateCar = ()=>{
car.value = '自行车';
}
8、pinia选择式API
vue3中pinia是个大菠萝,看着这么可爱的卡通形象,似乎学习pinia的知识也不是那么枯燥了。
Pinia中文文档
pinia类似于vue2中的vuex
vuex:集中式管理状态容器,可以实现任意组件之间的通信
核心概念:state、mutations、actions、getters、modules
pinia:集中式管理状态容器,可以实现任意组件之间的通信
核心概念:state、actions、getters
1、src目录下新建store文件夹,新建index.ts及modules文件下,目录如下:
index.ts
//创建大仓库
import { createPinia } from 'pinia';
//createPinia方法可以用于创建大仓库
let store = createPinia();
//对外暴露,安装仓库
export default store;
在main.ts中引入并使用
//引入仓库
import store from './store'
app.use(store)
引入成功后,可以看到
pinia写法:选择式API和组合式API
1、选择式API在modules中定义小的仓库,info
//defineStore 用于定义小仓库,从pinia中获取
//定义info小仓库
import { defineStore } from "pinia";
//defineStore需要传递两个参数,第一个参数:小仓库名字 第二个参数:小仓库配置对象
//defineStore方法执行会返回一个函数,函数作用就是让组件可以获取到仓库数据
let useInfoStore = defineStore("info", {
//存储数据:state,vuex中state是对象写法,pinia中是函数写法,函数返回的对象才是给组件提供的数据
state: () => {
return {
count: 99,
arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
}
},
actions: {
//vuex中commit提交,mutation去修改,pinia中没有mutation,直接在action中修改
//注意:函数没有context上下文对象,pinia中使用this获取state中的数据
//没有commit、没有mutations去修改数据
updateNum(a: number, b: number) {
this.count += a;
}
},
getters: {
total() {
let result:any = this.arr.reduce((prev: number, next: number) => {
return prev + next;
}, 0);
return result;
}
}
});
//对外暴露方法
export default useInfoStore;
在组件中使用,当数据改变的时候,所有使用的组件中的数据都会一同改变
import useInfoStore from "../../store/modules/info";
//获取小仓库对象
let infoStore = useInfoStore();
console.log(infoStore);
修改数据
<button @click="updateCount">点击我修改仓库数据</button>
```bash
//修改数据方法
const updateCount = () => {
//可以直接这么写:
//infoStore.count++
//用$patch方法用一个新的对象替换原来的对象
infoStore.$dispath({
count:1111
})
//仓库调用自身的方法去修改仓库的数据,在info.ts的actions中定义这个方法
infoStore.updateNum(66,77);
};
2、组合式API,在modules中定义小的仓库,todo
//定义组合式API仓库
import { defineStore } from "pinia";
import { ref, computed,watch} from 'vue';
//创建小仓库
let useTodoStore = defineStore('todo', () => {
let todos = ref([{ id: 1, title: '吃饭' }, { id: 2, title: '睡觉' }, { id: 3, title: '打豆豆' }]);
let arr = ref([1,2,3,4,5]);
const total = computed(() => {
return arr.value.reduce((prev, next) => {
return prev + next;
}, 0)
})
//务必要返回一个对象:属性与方法可以提供给组件使用
return {
todos,
arr,
total,
updateTodo() {
todos.value.push({ id: 4, title: '组合式API方法' });
}
}
});
export default useTodoStore;
在组件中使用
<p @click="updateTodo">{{ todoStore.arr }}{{todoStore.total}}</p>
import useInfoStore from "../../store/modules/info";
//获取小仓库对象
let infoStore = useInfoStore();
//引入组合式API函数仓库
import useTodoStore from "../../store/modules/todo";
let todoStore = useTodoStore();
//点击p段落去修改仓库的数据
const updateTodo = () => {
todoStore.updateTodo();
};
9、插槽
插槽有三种:默认插槽,具名插槽,作用域插槽
默认插槽,在子组件中放置slot,在父组件的子组件标签中添加的任意内容都会显示在插槽位置
1、默认插槽
子组件:
<template>
<div class="box">
<h1>我是子组件默认插槽</h1>
<!-- 默认插槽 -->
<slot></slot>
</div>
</template>
父组件
<template>
<div class="box">
<Test>
<div>
<pre>大江东去浪淘尽,千古分流人物</pre>
</div>
</Test>
</div>
</template>
2、具名插槽
具名插槽就是有名字的插槽,子组件将坑位留好,父组件根据插槽名对应的添置内容
v-slot 写法等同于#,v-slot=a相当于#a
子组件
<template>
<div class="box">
<h1>具名插槽填充数据a</h1>
<slot name="a"></slot>
<h1>具名插槽填充数据b</h1>
<slot name="b"></slot>
<h1>具名插槽填充数据</h1>
</div>
</template>
父组件
<template>
<div>
<h1>slot</h1>
<Test1 :todos="todos">
<template v-slot="{ $row, $index }">
<p :style="{ color: $row.done ? 'green' : 'red' }">
{{ $row.title }}--{{ $index }}
</p>
</template>
</Test1>
<Test>
<!-- 具名插槽填充a -->
<template #a>
<div>我是填充具名插槽a位置结构</div>
</template>
<!-- 具名插槽填充b v-slot指令可以简化为# -->
<template #b>
<div>我是填充具名插槽b位置结构</div>
</template>
</Test>
</div>
</template>
3、作用域插槽
作用域插槽我多花了点时间去理解,似乎和之前vue2的理解有点不一样,也有可能是在使用vue2的时候没有特别关注作用域插槽的实现。
作用域插槽是可以传递数据的插槽,子组件可以将数据回传给父组件。父组件可以决定这些回传的数据以何种结构或外观在子组件内部去展示。
举个例子,当我们封装了一个列表组件并配有一些查询的输入框或下拉框,a调用页面需要:使用者姓名,使用者部门这两个查询条件,b调用页面需要:商品名称,商品编号这两个查询条件,而除了查询条件不同,其他的都一样,我们不可能封装很多个子组件,那就没有复用的意义了。这个时候就可以使用作用域插槽,在父页面的时候将数据传给子组件,子组件通过作用域插槽将数据回传给父组件,父组件的子组件标签内部通过template标签展示,从而实现展示不同的查询条件。
父组件给子组件传递todos数组
<Test1 :todos="todos">
</Test1>
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 },
]);
子组件接收并在内部使用
<template>
<div class="box">
<ul>
<li v-for="(item, index) in todos" :key="item.id">
</li>
</ul>
</div>
</template>
defineProps(["todos"]);
当我们开始在子组件内部使用作用域插槽,对象中是key和value对应,$row 和 $index相当于是我们定义的key,后面跟的是对应的value。
<template>
<div class="box">
<ul>
<li v-for="(item, index) in todos" :key="item.id">
<slot :$row="item" :$index="index"></slot>
</li>
</ul>
</div>
</template>
在父组件的子组件标签内部使用插槽回传的数据
<Test1 :todos="todos">
<template v-slot="{ $row, $index }">
<p :style="{ color: $row.done ? 'green' : 'red' }">
{{ $row.title }}--{{ $index }}
</p>
</template>
</Test1>
可能会有疑惑,为啥要写的这么复杂,要从父组件传进去,再传出来。如果a调用页面需要的是绿色和红色两种颜色的判断,而b调用页面需要的是紫色和蓝色的判断,甚至是c,d等调用页面需要其他的颜色。甚至是包裹的标签这个需要p标签,那个需要div标签,甚至是其他的外观或者结构需要不同的展示
也许我们习惯的是从父组件传一个变量到子组件,在子组件内部通过v-if,v-else-if来判断不同的条件。不是很复杂的用哪种都可以,复杂的可以考虑作用域插槽,多一种实现方式,也不是非用不可。